lib/rackamole/mole.rb in rackamole-0.0.6 vs lib/rackamole/mole.rb in rackamole-0.0.7

- old
+ new

@@ -1,71 +1,152 @@ require 'hitimes' require 'json' require 'mongo' +# BOZO !! - Need args validator or use dsl as the args are out of control... module Rack class Mole - # Initialize The Mole with the possible options - # <tt>:app_name</tt> - The name of the application [Default: Moled App] - # <tt>:environment</tt> - The environment for the application ie :environment => RAILS_ENV - # <tt>:perf_threshold</tt> - Any request taking longer than this value will get moled [Default: 10] - # <tt>:moleable</tt> - Enable/Disable the MOle [Default:true] - # <tt>:store</tt> - The storage instance ie log file or mongodb [Default:stdout] - # <tt>:user_key</tt> - If session is enable, the session key for the user name or user_id. ie :user_key => :user_name + # Initialize The Mole rack component. It is recommended that you specify at a minimum a user_key to track + # interactions on a per user basis. If you wish to use the mole for the same application in different + # environments you should set the environment attribute RAILS_ENV for example. The perf_threshold setting + # is also recommended to get performance notifications should your web app start sucking. + # By default your app will be moleable upon installation and you should see mole features spewing out in your + # logs. This is the default setting. Alternatively you can store mole information in a mongo database by + # specifying a different store option. + # + # === Options + # + # :app_name :: The name of the application (Default: Moled App) + # :environment :: The environment for the application ie :environment => RAILS_ENV + # :perf_threshold :: Any request taking longer than this value will get moled. Default: 10secs + # :moleable :: Enable/Disable the MOle (Default:true) + # :store :: The storage instance ie log file or mongodb [Default:stdout] + # :user_key :: If sessions are enable, this represents the session key for the user name or + # user_id. + # == + # If the username resides in the session hash with key :user_name the you can use: + # :user_key => :user_name + # Or you can eval it on the fly - though this will be much slower and not recommended + # :user_key => { :session_key => :user_id, :extractor => lambda{ |id| User.find( id ).name} } + # == + # + # :twitter_auth :: You can setup the MOle twit interesting events to a private (public if you indulge pain!) twitter account. + # Specified your twitter account information using a hash with :username and :password key + # :twitt_on :: You must configure your twitter auth and configuration using this hash. By default this option is disabled. + # == + # :twitt_on => { :enabled => false, :features => [Rackamole.perf, Rackamole.fault] } + # == + # ==== BOZO! currently there is not support for throttling or monitoring these alerts. + # == + # :emails :: The mole can be configured to send out emails bases on interesting mole features. + # This feature uses actionmailer. You must specify a hash for the from and to options. + # == + # :emails => { :from => 'fred@acme.com', :to => ['blee@acme.com', 'doh@acme.com'] } + # == + # :mail_on :: Hash for email alert triggers. May be enabled or disabled per env settings. Default is disabled + # == + # :mail_on => {:enabled => true, :features => [Rackamole.perf, Rackamole.fault] } def initialize( app, opts={} ) @app = app init_options( opts ) + validate_options end - + + # Entering the MOle zone... + # Watches incoming requests and report usage information. The mole will also track request that + # are taking longer than expected and also report any requests that are raising exceptions. def call( env ) # Bail if application is not moleable return @app.call( env ) unless moleable? status, headers, body = nil elapsed = Hitimes::Interval.measure do begin status, headers, body = @app.call( env ) rescue => boom env['mole.exception'] = boom - @store.mole( mole_info( env, elapsed, status, headers, body ) ) + mole_feature( env, elapsed, status, headers, body ) raise boom end end - @store.mole( mole_info( env, elapsed, status, headers, body ) ) + mole_feature( env, elapsed, status, headers, body ) return status, headers, body end # =========================================================================== private + attr_reader :options #:nodoc: + # Load up configuration options def init_options( opts ) - options = default_options.merge( opts ) - @environment = options[:environment] - @perf_threshold = options[:perf_threshold] - @moleable = options[:moleable] - @app_name = options[:app_name] - @user_key = options[:user_key] - @store = options[:store] - @excluded_paths = options[:excluded_paths] + @options = default_options.merge( opts ) end # Mole default options def default_options { :app_name => "Moled App", :excluded_paths => [/.?\.ico/, /.?\.png/], :moleable => true, :perf_threshold => 10, - :store => Rackamole::Store::Log.new + :store => Rackamole::Store::Log.new, + :twitt_on => { :enabled => false, :features => [Rackamole.perf, Rackamole.fault] }, + :mail_on => { :enabled => false, :features => [Rackamole.perf, Rackamole.fault] } } end - + + # Validates all configured options... Throws error if invalid configuration + def validate_options + %w[app_name moleable perf_threshold store].each do |k| + raise "[M()le] -- Unable to locate required option key `#{k}" unless options[k.to_sym] + end + end + + # Send moled info to store and potentially send out alerts... + def mole_feature( env, elapsed, status, headers, body ) + attrs = mole_info( env, elapsed, status, headers, body ) + + # send info to configured store + options[:store].mole( attrs ) + + # send email alert ? + if configured?( :emails, [:from, :to] ) and alertable?( options[:mail_on], attrs[:type] ) + Rackamole::Alert::Emole.deliver_alert( options[:emails][:from], options[:emails][:to], attrs ) + end + + # send twitter alert ? + if configured?( :twitter_auth, [:username, :password] ) and alertable?( options[:twitt_on], attrs[:type] ) + twitt.send_alert( attrs ) + end + rescue => boom + $stderr.puts "!! MOLE RECORDING CRAPPED OUT !! -- #{boom}" + boom.backtrace.each { |l| $stderr.puts l } + end + + # Check if an options is set and configured + def configured?( key, configs ) + return false unless options[key] + configs.each { |c| return false unless options[key][c] } + true + end + + # Check if feature should be send to alert clients ie email or twitter + def alertable?( filters, type ) + return false if !filters or filters.empty? or !filters[:enabled] + filters[:features].include?( type ) + end + + # Create or retrieve twitter client + def twitt + @twitt ||= Rackamole::Alert::Twitt.new( options[:twitter_auth][:username], options[:twitter_auth][:password] ) + end + # Check if this request should be moled according to the exclude filters def mole_request?( request ) - @excluded_paths.each do |exclude_path| + options[:excluded_paths].each do |exclude_path| return false if request.path.match( exclude_path ) end true end @@ -85,31 +166,32 @@ user_id = nil user_name = nil # BOZO !! This could be slow if have to query db to get user name... # Preferred store username in session and give at key - if session and @user_key - if @user_key.instance_of? Hash - user_id = session[ @user_key[:session_key] ] - if @user_key[:extractor] - user_name = @user_key[:extractor].call( user_id ) + user_key = options[:user_key] + if session and user_key + if user_key.instance_of? Hash + user_id = session[ user_key[:session_key] ] + if user_key[:extractor] + user_name = user_key[:extractor].call( user_id ) end else - user_name = session[@user_key] + user_name = session[user_key] end end - - info[:app_name] = @app_name - info[:environment] = @environment || "Unknown" + + info[:type] = (elapsed and elapsed > options[:perf_threshold] ? Rackamole.perf : Rackamole.feature) + info[:app_name] = options[:app_name] + info[:environment] = options[:environment] || "Unknown" info[:user_id] = user_id if user_id info[:user_name] = user_name || "Unknown" info[:ip] = ip info[:browser] = id_browser( user_agent ) info[:host] = env['SERVER_NAME'] info[:software] = env['SERVER_SOFTWARE'] info[:request_time] = elapsed if elapsed - info[:performance] = (elapsed and elapsed > @perf_threshold) info[:url] = request.url info[:method] = env['REQUEST_METHOD'] info[:path] = request.path info[:route_info] = route if route @@ -127,17 +209,16 @@ # Check if an exception was raised. If so consume it and clear state exception = env['mole.exception'] if exception info[:ruby_version] = %x[ruby -v] + info[:fault] = exception.to_s info[:stack] = trim_stack( exception ) + info[:type] = Rackamole.fault env['mole.exception'] = nil end info - rescue => boom - $stderr.puts "!! MOLE RECORDING CRAPPED OUT !! -- #{boom}" - boom.backtrace.each { |l| $stderr.puts l } end # Attempts to detect browser type from agent info. # BOZO !! Probably more efficient way to do this... def browser_types() @browsers ||= [ 'Firefox', 'Safari', 'MSIE 8.0', 'MSIE 7.0', 'MSIE 6.0', 'Opera', 'Chrome' ] end @@ -160,10 +241,10 @@ return request_env['HTTP_X_FORWARDED_FOR'] || request_env['REMOTE_ADDR'], request_env['HTTP_USER_AGENT'] end # Checks if this application is moleable def moleable? - @moleable + options[:moleable] end # Fetch route info if any... def get_route( request ) return nil unless defined?( RAILS_ENV ) \ No newline at end of file