require 'mongo' # TODO !! Need to deal with auth # BOZO !! Deal with indexes here ? module Rackamole module Store # Mongo adapter. Stores mole info to a mongo database. class MongoDb attr_reader :host, :port, :db_name, :database, :logs, :features, :users #:nodoc: # Initializes the mongo db store. MongoDb can be used as a persitent store for # mole information. This is a preferred store for the mole as it will allow you # to gather up reports based on application usage, perf or faults... # # Setup rackamole to use the mongodb store as follows: # # config.middleware.use Rack::Mole, { :store => Rackamole::Store::MongoDb.new } # # === Options # # :host :: The name of the host running the mongo server. Default: localhost # :port :: The port for the mongo server instance. Default: 27017 # :db_name :: The name of the mole databaase. Default: mole_mdb # def initialize( options={} ) opts = default_options.merge( options ) validate_options( opts ) init_mongo( opts ) end def to_yaml( opts={} ) YAML::quick_emit( object_id, opts ) do |out| out.map( taguri, to_yaml_style ) do |map| map.add( :host , host ) map.add( :port , port ) map.add( :db_name , db_name ) end end end # Dump mole info to a mongo database. There are actually 2 collections # for mole information. Namely features and logs. The features collection hold # application and feature information and is referenced in the mole log. The logs # collections holds all information that was gathered during the request # such as user, params, session, request time, etc... def mole( arguments ) return if arguments.empty? unless @connection init_mongo( :host => host, :port => port, :db_name => db_name ) end # get a dup of the args since will mock with the original args = arguments.clone # dump request info to mongo save_log( save_user( args ), save_feature( args ), args ) rescue => mole_boom $stderr.puts "MOLE STORE CRAPPED OUT -- #{mole_boom}" $stderr.puts mole_boom.backtrace.join( "\n " ) end # ======================================================================= private # Clear out mole database content ( Careful there - testing only! ) def reset! logs.remove features.remove users.remove end def init_mongo( opts ) @host = opts[:host] @port = opts[:port] @db_name = opts[:db_name] @connection = Mongo::Connection.new( @host, @port, :logger => opts[:logger] ) @database = @connection.db( @db_name ) @features = database.collection( 'features' ) @logs = database.collection( 'logs' ) @users = database.collection( 'users' ) end # Validates option hash. def validate_options( opts ) %w[host port db_name].each do |option| unless opts[option.to_sym] raise "[MOle] Mongo store configuration error -- You must specify a value for option `:#{option}" end end end # Set up mongo default options ie localhost host, default mongo port and # the database being mole_mdb def default_options { :host => 'localhost', :port => Mongo::Connection::DEFAULT_PORT } end # Find or create a moled user... # BOZO !! What to do if user name changed ? def save_user( args ) user_id = args.delete( :user_id ) if args.has_key?( :user_id ) user_name = args.delete( :user_name ) if args.has_key?( :user_name ) row = {} if user_id and user_name row = { min_field( :user_id ) => user_id, min_field( :user_name ) => user_name } else row = { min_field( :user_name ) => user_name } end user = users.find_one( row, :fields => ['_id'] ) return user['_id'] if user now = args[:created_at] row[min_field(:date_id)] = "%4d%02d%02d" % [now.year, now.month, now.day] users.save( row ) end # Find or create a mole feature... def save_feature( args ) app_name = args.delete( :app_name ) route_info = args.delete( :route_info ) environment = args.delete( :environment ) row = { min_field(:app_name) => app_name, min_field(:env) => environment.to_s } if route_info row[min_field(:controller)] = route_info[:controller] row[min_field(:action)] = route_info[:action] else row[min_field(:context)] = args.delete( :path ) end feature = features.find_one( row, :fields => ['_id'] ) return feature['_id'] if feature now = args[:created_at] row[min_field(:date_id)] = "%4d%02d%02d" %[now.year, now.month, now.day] features.save( row ) end # Insert a new feature in the db # NOTE : Using min key to reduce storage needs. I know not that great for higher level api's :-( # also saving date and time as ints. same deal... def save_log( user_id, feature_id, args ) now = args.delete( :created_at ) row = { min_field( :type ) => args[:type], min_field( :feature_id ) => feature_id, min_field( :user_id ) => user_id, min_field( :date_id ) => "%4d%02d%02d" %[now.year, now.month, now.day], min_field( :time_id ) => "%02d%02d%02d" %[now.hour, now.min, now.sec] } args.each do |k,v| row[min_field(k)] = check_hash( v ) if v end logs.save( row ) end # Check for invalid key format - ie something that will choke mongo # case a.b.c => a_b_c def ensure_valid_key( key ) key.to_s.index( /\./ ) ? key.to_s.gsub( /\./, '_' ) : key end # Check def check_hash( value ) return value unless value.is_a?( Hash ) value.keys.inject({}){ |h,k| h[ensure_valid_key(k)] = value[k];h } end # For storage reason minify the json to save space... def min_field( field ) Rackamole::Store::MongoDb.field_map[field] || field end # Normalize all accessors to 3 chars. def self.field_map @field_map ||= { :env => :env, :app_name => :app, :context => :ctx, :controller => :ctl, :action => :act, :type => :typ, :feature_id => :fid, :date_id => :did, :time_id => :tid, :user_id => :uid, :user_name => :una, :browser => :bro, :machine => :mac, :host => :hos, :software => :sof, :request_time => :rti, :performance => :per, :method => :met, :path => :pat, :session => :ses, :params => :par, :ruby_version => :ver, :fault => :msg, :stack => :sta, :created_at => :cro, :status => :sts, :headers => :hdr, :body => :bod } end end end end