require 'hitimes' require 'json' require 'mongo' module Rack class Mole # Initialize The Mole with the possible 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: 10] # :moleable - Enable/Disable the MOle [Default:true] # :store - The storage instance ie log file or mongodb [Default:stdout] # :user_key - If session is enable, the session key for the user name or user_id. ie :user_key => :user_name def initialize( app, opts={} ) @app = app init_options( opts ) end 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 ) ) raise boom end end @store.mole( mole_info( env, elapsed, status, headers, body ) ) return status, headers, body end # =========================================================================== private # 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] 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 } end # Check if this request should be moled according to the exclude filters def mole_request?( request ) @excluded_paths.each do |exclude_path| return false if request.path.match( exclude_path ) end true end # Extract interesting information from the request def mole_info( env, elapsed, status, headers, body ) request = Rack::Request.new( env ) info = OrderedHash.new # dump( env ) return info unless mole_request?( request ) session = env['rack.session'] route = get_route( request ) ip, user_agent = identify( env ) 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 ) end else user_name = session[@user_key] end end info[:app_name] = @app_name info[:environment] = @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 # Dump request params unless request.params.empty? info[:params] = OrderedHash.new request.params.keys.sort.each { |k| info[:params][k.to_sym] = request.params[k].to_json } end # Dump session var if session and !session.empty? info[:session] = OrderedHash.new session.keys.sort{ |a,b| a.to_s <=> b.to_s }.each { |k| info[:session][k.to_sym] = session[k].to_json } end # 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[:stack] = trim_stack( exception ) 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 def id_browser( user_agent ) return "N/A" if !user_agent or user_agent.empty? browser_types.each do |b| return b if user_agent.match( /.*?#{b.gsub(/\./,'\.')}.*?/ ) end "N/A" end # Trim stack trace def trim_stack( boom ) boom.backtrace[0...4] end # Identify request ie ip and browser configuration def identify( request_env ) 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 end # Fetch route info if any... def get_route( request ) return nil unless defined?( RAILS_ENV ) # Check for invalid route exception... begin return ::ActionController::Routing::Routes.recognize_path( request.path, {:method => request.request_method.downcase.to_sym } ) rescue return nil end end # Dump env to stdout # def dump( env, level=0 ) # env.keys.sort{ |a,b| a.to_s <=> b.to_s }.each do |k| # value = env[k] # if value.respond_to?(:each_pair) # puts "%s %-#{40-level}s" % [' '*level,k] # dump( env[k], level+1 ) # elsif value.instance_of?(::ActionController::Request) or value.instance_of?(::ActionController::Response) # puts "%s %-#{40-level}s %s" % [ ' '*level, k, value.class ] # else # puts "%s %-#{40-level}s %s" % [ ' '*level, k, value.inspect ] # end # end # end end end