├── spec ├── spec_helper.rb ├── data │ └── create_secure_db.rb ├── test_configs │ ├── flawed_config.yml │ └── rackamole_test.yml ├── rackamole │ ├── stash │ │ ├── perf_spec.rb │ │ ├── fault_spec.rb │ │ └── collector_spec.rb │ ├── interceptor_spec.rb │ ├── logger_spec.rb │ ├── store │ │ ├── log_spec.rb │ │ └── mongo_db_spec.rb │ ├── alert │ │ ├── growl_spec.rb │ │ ├── twitt_spec.rb │ │ └── emole_spec.rb │ ├── utils │ │ └── agent_detect_spec.rb │ └── mole_spec.rb ├── rackamole_spec.rb └── expected_results │ ├── mole_feature.log │ ├── mole_perf.log │ └── mole_exception.log ├── lib ├── rackamole │ ├── stash │ │ ├── fault.rb │ │ ├── perf.rb │ │ ├── base.rb │ │ └── collector.rb │ ├── store.rb │ ├── alert │ │ ├── templates │ │ │ └── alert.erb │ │ ├── twitt.rb │ │ ├── growl.rb │ │ └── emole.rb │ ├── interceptor.rb │ ├── store │ │ ├── log.rb │ │ └── mongo_db.rb │ ├── utils │ │ └── agent_detect.rb │ ├── logger.rb │ └── mole.rb └── rackamole.rb ├── Gemfile ├── Rakefile ├── History.txt └── README.rdoc /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | require 'rack/test' 3 | 4 | require File.join(File.dirname(__FILE__), %w[.. lib rackamole]) 5 | 6 | Spec::Runner.configure do |config| 7 | end -------------------------------------------------------------------------------- /spec/data/create_secure_db.rb: -------------------------------------------------------------------------------- 1 | # BOZO !! Rake task 2 | require 'rubygems' 3 | require 'mongo' 4 | 5 | con = Mongo::Connection.new( 'localhost' ) 6 | db = con.db( 'sec_app_test_mdb' ) 7 | db.add_user( 'fred', 'letmein' ) -------------------------------------------------------------------------------- /lib/rackamole/stash/fault.rb: -------------------------------------------------------------------------------- 1 | module Rackamole::Stash 2 | class Fault < Rackamole::Stash::Base 3 | attr_reader :stack 4 | 5 | def initialize( path, stack, timestamp ) 6 | super( path, timestamp ) 7 | @stack = stack 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /lib/rackamole/store.rb: -------------------------------------------------------------------------------- 1 | # Provides various transient/persistent storage for the mole. 2 | # Currently, you can send moled info to the console, a file, or a mongo database. 3 | # Please see the various stores for more info... 4 | # 5 | Rackamole.require_all_libs_relative_to(__FILE__) -------------------------------------------------------------------------------- /lib/rackamole/stash/perf.rb: -------------------------------------------------------------------------------- 1 | module Rackamole::Stash 2 | class Perf < Rackamole::Stash::Base 3 | attr_reader :elapsed 4 | 5 | def initialize( path, elapsed, timestamp ) 6 | super( path, timestamp ) 7 | @elapsed = elapsed 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /spec/test_configs/flawed_config.yml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Flawed config 3 | # ----------------------------------------------------------------------------- 4 | 5 | # ----------------------------------------------------------------------------- 6 | # Shared configuration 7 | defaults: &defaults 8 | :moleable: true 9 | :app_name: TestApp 10 | -------------------------------------------------------------------------------- /lib/rackamole/alert/templates/alert.erb: -------------------------------------------------------------------------------- 1 | <%= feature_type %> alert on application <%= args[:app_name] %> on host <%=args[:host] %> 2 | 3 | <% %w(what server params session browser headers client).each do |heading| %> 4 | <%= section( heading ) -%> 5 | <% end -%> 6 | 7 | =============================================================== 8 | Powered by Rackamole. This message was generated automatically. 9 | Please do not respond directly. 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'rake' 4 | gem "logging" 5 | gem "hitimes" 6 | gem "mongo" , ">= 1.0.1" 7 | gem "bson" , ">= 1.0.1" 8 | gem "bson_ext", ">= 1.0.1" 9 | gem "chronic" 10 | gem "twitter4r", ">= 0.3.0" 11 | gem "erubis" 12 | gem "mail" 13 | gem "ruby-growl" 14 | 15 | group :development do 16 | gem "bones" 17 | gem "bones-git" 18 | gem "bones-extras" 19 | end 20 | 21 | group :test do 22 | # gem "simplecov" 23 | # gem "rcov" 24 | gem "rspec" 25 | end -------------------------------------------------------------------------------- /spec/rackamole/stash/perf_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. spec_helper])) 2 | require 'chronic' 3 | 4 | describe Rackamole::Stash::Perf do 5 | before( :all ) do 6 | @now = Chronic.parse( "11/27/2009" ) 7 | end 8 | 9 | it "should record perf information correctly" do 10 | perf = Rackamole::Stash::Perf.new( "/", 10.0, @now ) 11 | perf.send( :path ).should == "/" 12 | perf.send( :elapsed ).should == 10.0 13 | perf.send( :timestamp ).should == @now 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/rackamole_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[spec_helper])) 2 | 3 | describe Rackamole do 4 | before( :all ) do 5 | @root = ::File.expand_path( ::File.join(::File.dirname(__FILE__), ".." ) ) 6 | end 7 | 8 | it "is versioned" do 9 | Rackamole.version.should =~ /\d+\.\d+\.\d+/ 10 | end 11 | 12 | it "generates a correct path relative to root" do 13 | Rackamole.path( "mole.rb" ).should == ::File.join(@root, "mole.rb" ) 14 | end 15 | 16 | it "generates a correct path relative to lib" do 17 | Rackamole.libpath(%w[ rackmole mole.rb]).should == ::File.join(@root, "lib", "rackmole", "mole.rb") 18 | end 19 | 20 | end -------------------------------------------------------------------------------- /spec/expected_results/mole_feature.log: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------------------------------------- 2 | FEATURE 3 | Type : 0 4 | App_name : "Test app" 5 | Environment : :test 6 | Ip : "1.1.1.1" 7 | Browser : {:name=>"Ibrowse", :version=>"1.X"} 8 | Machine : {:platform=>"Blee", :os=>"Windoze", :version=>"10.0"} 9 | User_id : 100 10 | User_name : "Fernand" 11 | Request_time : 1.0 12 | Url : "http://test_me/" 13 | Path : "/fred" 14 | Method : "GET" 15 | Params : {:blee=>"\"duh\""} 16 | Session : {:fred=>"10"} 17 | -------------------------------------------------------------------------------- /spec/expected_results/mole_perf.log: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------------------------------------- 2 | PERFORMANCE 3 | Type : 1 4 | App_name : "Test app" 5 | Environment : :test 6 | Ip : "1.1.1.1" 7 | Browser : {:name=>"Ibrowse", :version=>"1.X"} 8 | Machine : {:platform=>"Blee", :os=>"Windoze", :version=>"10.0"} 9 | User_id : 100 10 | User_name : "Fernand" 11 | Request_time : 1.0 12 | Url : "http://test_me/" 13 | Path : "/fred" 14 | Method : "GET" 15 | Params : {:blee=>"\"duh\""} 16 | Session : {:fred=>"10"} 17 | -------------------------------------------------------------------------------- /spec/rackamole/stash/fault_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. spec_helper])) 2 | require 'chronic' 3 | 4 | describe Rackamole::Stash::Fault do 5 | before( :all ) do 6 | @now = Chronic.parse( "11/27/2009" ) 7 | end 8 | 9 | it "should record fault information correctly" do 10 | begin 11 | raise "Oh snap!" 12 | rescue => boom 13 | fault = Rackamole::Stash::Fault.new( "/", boom.backtrace.first, @now ) 14 | fault.send( :path ).should == "/" 15 | fault.send( :stack ).should == File.join( File.dirname(__FILE__), "fault_spec.rb:11:in `block (2 levels) in '" ) 16 | fault.send( :timestamp ).should == @now 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/rackamole/stash/base.rb: -------------------------------------------------------------------------------- 1 | module Rackamole::Stash 2 | # Stash mole information into the env. These objects are meant to track 3 | # instances of a similar event occurring in the application so that alerts 4 | # are kept under control when shit hits the fan... 5 | class Base 6 | attr_reader :path, :timestamp, :count 7 | 8 | # =======================================================================-- 9 | protected 10 | 11 | def initialize( path, timestamp ) 12 | @path = path 13 | @count = 1 14 | @timestamp = timestamp 15 | end 16 | 17 | public 18 | 19 | # Update count and timestamp 20 | def update( timestamp ) 21 | @timestamp = timestamp 22 | @count += 1 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /spec/expected_results/mole_exception.log: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------------------------------------- 2 | FAULT 3 | Type : 2 4 | App_name : "Test app" 5 | Environment : :test 6 | Ip : "1.1.1.1" 7 | Browser : {:name=>"Ibrowse", :version=>"1.X"} 8 | Machine : {:platform=>"Blee", :os=>"Windoze", :version=>"10.0"} 9 | User_id : 100 10 | User_name : "Fernand" 11 | Request_time : 1.0 12 | Url : "http://test_me/" 13 | Path : "/fred" 14 | Method : "GET" 15 | Params : {:blee=>"\"duh\""} 16 | Session : {:fred=>"10"} 17 | Fault : "Shiet" 18 | Stack : ["Oh snap!"] 19 | Ruby_version : "ruby 1.8.6 (2007-03-13 patchlevel 0) [i686-darwin8.10.1]" 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bones' 3 | rescue LoadError 4 | abort '### Please install the "bones" gem ###' 5 | end 6 | 7 | task :default => 'spec:run' 8 | task 'gem:release' => 'spec:run' 9 | 10 | Bones { 11 | name 'rackamole' 12 | authors 'Fernand Galiana' 13 | readme_file 'README.rdoc' 14 | email 'fernand.galiana@gmail.com' 15 | url 'http://www.rackamole.com' 16 | # spec.opts %w[--color] 17 | ruby_opts %w[-W0] 18 | 19 | # Dependencies 20 | depend_on "logging" , ">= 1.2.2" 21 | depend_on "hitimes" , ">= 1.0.3" 22 | depend_on "mongo" , ">= 1.0.1" 23 | depend_on "bson" , ">= 1.0.1" 24 | depend_on "bson_ext" , ">= 1.0.1" 25 | depend_on "chronic" , ">= 0.2.3" 26 | depend_on "twitter4r" , ">= 0.3.0" 27 | depend_on "erubis" , ">= 2.6.0" 28 | depend_on "mail" , ">= 2.1.3" 29 | depend_on "ruby-growl" , ">= 2.0" 30 | } -------------------------------------------------------------------------------- /lib/rackamole/interceptor.rb: -------------------------------------------------------------------------------- 1 | module Rackamole 2 | module Interceptor 3 | 4 | # === For Rails only! 5 | # 6 | # Rails handles raised exception in a special way. 7 | # Thus special care need to be taken to enable exception to bubble up 8 | # to the mole. 9 | # 10 | # In order for the mole to trap framework exception, you must include 11 | # the interceptor in your application controller. 12 | # ie include Rackamole::Interceptor 13 | def self.included( base ) 14 | base.send( :alias_method_chain, :rescue_action_in_public, :mole ) 15 | base.send( :alias_method_chain, :rescue_action_locally, :mole ) 16 | end 17 | 18 | private 19 | 20 | # Instructs the mole to trap the framework exception 21 | def rescue_action_locally_with_mole( exception ) 22 | # Stuff the exception in the env for mole rack retrieval 23 | request.env['mole.exception'] = exception 24 | rescue_action_locally_without_mole( exception ) 25 | end 26 | 27 | # Instructs the mole to trap the framework exception 28 | def rescue_action_in_public_with_mole( exception ) 29 | # Stuff the exception in the env for mole rack retrieval 30 | request.env['mole.exception'] = exception 31 | rescue_action_in_public_without_mole( exception ) 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /spec/test_configs/rackamole_test.yml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Rackamole Test rack configuration file 3 | # ----------------------------------------------------------------------------- 4 | 5 | # ----------------------------------------------------------------------------- 6 | # Shared configuration 7 | defaults: &defaults 8 | :moleable: true 9 | :app_name: TestApp 10 | :user_key: :user_name 11 | :twitter: 12 | :username: bumblebtuna 13 | :password: secret 14 | :alert_on: 15 | - <%= Rackamole.perf %> 16 | :email: 17 | :from: Bozo@acme.com 18 | :to: 19 | - 'fernand@acme.com' 20 | :alert_on: 21 | - <%= Rackamole.fault %> 22 | 23 | # ----------------------------------------------------------------------------- 24 | # 25 | development: 26 | <<: *defaults 27 | :perf_threshold: 2 28 | 29 | # ----------------------------------------------------------------------------- 30 | # Turn mole off in test 31 | test: 32 | :moleable: false 33 | 34 | # ----------------------------------------------------------------------------- 35 | # Use mongo based store in production env 36 | production: 37 | <<: *defaults 38 | :perf_threshold: 5 39 | :store: !ruby/object:Rackamole::Store::MongoDb 40 | host: fred 41 | port: 10 42 | db_name: mole_fred_production 43 | username: fred 44 | password: secret 45 | -------------------------------------------------------------------------------- /spec/rackamole/interceptor_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. spec_helper])) 2 | 3 | require 'ostruct' 4 | 5 | # Test application... 6 | class Fred 7 | def rescue_action_in_public( exception ) 8 | end 9 | 10 | def rescue_action_locally( exception ) 11 | end 12 | 13 | def request 14 | @request ||= OpenStruct.new( :env => {} ) 15 | end 16 | 17 | def self.alias_method_chain(target, feature) 18 | alias_method "#{target}_without_#{feature}", target 19 | alias_method target, "#{target}_with_#{feature}" 20 | end 21 | 22 | include Rackamole::Interceptor 23 | end 24 | 25 | describe Rackamole::Interceptor do 26 | it "should include the correct methods" do 27 | Fred.instance_methods.should be_include( :rescue_action_locally_without_mole ) 28 | Fred.private_instance_methods.should be_include( :rescue_action_locally_with_mole ) 29 | 30 | Fred.instance_methods.should be_include( :rescue_action_in_public_without_mole ) 31 | Fred.private_instance_methods.should be_include( :rescue_action_in_public_with_mole ) 32 | end 33 | 34 | it "should set the env correctly when a local exception is raised" do 35 | fred = Fred.new 36 | fred.send( :rescue_action_locally, "Fred" ) 37 | fred.request.env['mole.exception'].should == "Fred" 38 | end 39 | 40 | it "should set the env correctly when an exception is raised" do 41 | fred = Fred.new 42 | fred.send( :rescue_action_in_public, "Fred" ) 43 | fred.request.env['mole.exception'].should == "Fred" 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/rackamole.rb: -------------------------------------------------------------------------------- 1 | module Rackamole 2 | 3 | # :stopdoc: 4 | VERSION = '0.3.9' 5 | LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR 6 | PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR 7 | # :startdoc: 8 | 9 | # Returns the version string for the library. 10 | def self.version 11 | VERSION 12 | end 13 | 14 | # Defines the default moled activity types 15 | def self.feature() 0; end 16 | def self.perf() 1; end 17 | def self.fault() 2; end 18 | 19 | # Returns the library path for the module. If any arguments are given, 20 | # they will be joined to the end of the libray path using 21 | # File.join. 22 | def self.libpath( *args ) 23 | args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten) 24 | end 25 | 26 | # Returns the lpath for the module. If any arguments are given, 27 | # they will be joined to the end of the path using 28 | # File.join. 29 | def self.path( *args ) 30 | args.empty? ? PATH : ::File.join(PATH, args.flatten) 31 | end 32 | 33 | # Utility method used to require all files ending in .rb that lie in the 34 | # directory below this file that has the same name as the filename passed 35 | # in. Optionally, a specific _directory_ name can be passed in such that 36 | # the _filename_ does not have to be equivalent to the directory. 37 | def self.require_all_libs_relative_to( fname, dir = nil ) 38 | dir ||= ::File.basename(fname, '.*') 39 | search_me = ::File.expand_path( 40 | ::File.join(::File.dirname(fname), dir, '**', '*.rb')) 41 | 42 | Dir.glob(search_me).sort.each {|rb| require rb} 43 | end 44 | 45 | end 46 | 47 | Rackamole.require_all_libs_relative_to(__FILE__) -------------------------------------------------------------------------------- /lib/rackamole/store/log.rb: -------------------------------------------------------------------------------- 1 | module Rackamole::Store 2 | class Log 3 | 4 | # Stores mole information to a log file or dump it to the console. All 5 | # available mole info will dumped to the logger. 6 | # 7 | # === Params: 8 | # file_name :: Specifies a file to send the logs to. By default mole info 9 | # will be sent out to stdout. 10 | def initialize( file_name=$stdout ) 11 | @logger = Rackamole::Logger.new( :log_file => file_name ) 12 | end 13 | 14 | # Dump mole info to logger 15 | # 16 | # === Params: 17 | # attrs :: The available moled information for a given feature 18 | def mole( attrs ) 19 | return if attrs.empty? 20 | 21 | display_head( attrs ) 22 | display_commons( attrs ) 23 | rescue => mole_boom 24 | log.error "MOLE STORE CRAPPED OUT -- #{mole_boom}" 25 | log.error mole_boom.backtrace.join( "\n " ) 26 | end 27 | 28 | # ======================================================================= 29 | private 30 | 31 | # dump moled info to log 32 | def display_commons( args ) 33 | args.each do |k,v| 34 | display_info( k.to_s.capitalize, v.inspect ) 35 | end 36 | end 37 | 38 | # retrieves logger instance 39 | def log 40 | @logger 41 | end 42 | 43 | # Console layout spacer 44 | def spacer() 20; end 45 | 46 | # Display mole type 47 | def display_head( args ) 48 | log.info "-"*100 49 | log.info case args[:type] 50 | when Rackamole.feature 51 | "FEATURE" 52 | when Rackamole.fault 53 | "FAULT" 54 | when Rackamole.perf 55 | "PERFORMANCE" 56 | end 57 | end 58 | 59 | # Output formating... 60 | def display_info( key, value ) 61 | log.info "%-#{spacer}s : %s" % [key, value] 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /spec/rackamole/stash/collector_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. spec_helper])) 2 | require 'chronic' 3 | 4 | describe Rackamole::Stash::Collector do 5 | before( :each ) do 6 | @now = Chronic.parse( "11/27/2009" ) 7 | @collector = Rackamole::Stash::Collector.new( "Fred", "test" ) 8 | end 9 | 10 | describe "#stash" do 11 | it "should record fault information correctly" do 12 | begin 13 | raise "Oh snap!" 14 | rescue => boom 15 | @collector.stash_fault( "/", boom.backtrace.first, @now ) 16 | @collector.send( :faults ).size.should == 1 17 | 18 | fault = @collector.send( :find_fault, "/", boom.backtrace.first ) 19 | fault.should_not be_nil 20 | fault.send( :path ).should == "/" 21 | fault.send( :stack ).should == File.join( File.dirname(__FILE__), "collector_spec.rb:13:in `block (3 levels) in '" ) 22 | fault.send( :timestamp ).should == @now 23 | end 24 | end 25 | 26 | it "should record perf information correctly" do 27 | @collector.stash_perf( "/", 10.0, @now ) 28 | @collector.send( :perfs ).size.should == 1 29 | 30 | perf = @collector.send( :find_perf, "/" ) 31 | perf.should_not be_nil 32 | perf.send( :path ).should == "/" 33 | perf.send( :elapsed ).should == 10.0 34 | perf.send( :timestamp ).should == @now 35 | end 36 | end 37 | 38 | describe "#expire" do 39 | before( :all ) do 40 | @now = Chronic.parse( "11/27/2009" ) 41 | @yesterday = Chronic.parse( "yesterday", :now => @now ) 42 | end 43 | 44 | it "should expire fault correctly" do 45 | begin 46 | raise "Oh snap!" 47 | rescue => boom 48 | @collector.stash_fault( "/", boom.backtrace.first, @yesterday ) 49 | @collector.send( :faults ).size.should == 1 50 | @collector.expire_faults! 51 | @collector.send( :faults ).size.should == 0 52 | end 53 | end 54 | 55 | it "should expire perf correctly" do 56 | @collector.stash_perf( "/", 10, @yesterday ) 57 | @collector.send( :perfs ).size.should == 1 58 | @collector.expire_perfs! 59 | @collector.send( :perfs ).size.should == 0 60 | end 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /spec/rackamole/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. spec_helper])) 2 | 3 | require 'stringio' 4 | 5 | describe Rackamole::Logger do 6 | it "raises an error if the email addresses passed in is empty" do 7 | lambda { Rackamole::Logger.new( { :email_alerts_to => [] } ) }.should raise_error( Rackamole::Logger::ConfigurationError ) 8 | end 9 | 10 | it "configures an email appender if :email_alerts is set" do 11 | l = Rackamole::Logger.new( { :logger_name => "Test2", :email_alerts_to => "fernand@invalid.address", :email_alert_level => :off }) 12 | l.email_appender.should_not == nil 13 | end 14 | 15 | it "does not configure an email appender if :email_alerts is not set" do 16 | l = Rackamole::Logger.new( { :logger_name => "Test3" }) 17 | lambda { l.email_appender }.should raise_error( Rackamole::Logger::ConfigurationError ) 18 | end 19 | 20 | it "raises an error if an invalid object is passed in for the :log_file" do 21 | lambda { l = Rackamole::Logger.new( { :log_file => Object.new } ) }.should raise_error( Rackamole::Logger::ConfigurationError ) 22 | end 23 | 24 | it "logs to an IO stream if given" do 25 | io = StringIO.new 26 | l = Rackamole::Logger.new( { :log_file => io, :logger_name => "Test4" }) 27 | l.info "This is a test io message" 28 | io.string.split("\n").should have(1).item 29 | io.string.should =~ /This is a test io message/ 30 | io.string.should =~ /INFO/ 31 | end 32 | 33 | it "logs to a file if given a file name to log to" do 34 | log_file = "/tmp/mole_logger_test.log" 35 | FileUtils.rm( log_file ) if File.exists?( log_file ) 36 | l = Rackamole::Logger.new({ :log_file => log_file, :logger_name => "Test5" }) 37 | l.info "This is a test log file message" 38 | 39 | log_lines = IO.readlines(log_file) 40 | 41 | log_lines.should have(1).items 42 | log_lines.first.should =~ /This is a test log file message/ 43 | log_lines.first.should =~ /INFO/ 44 | end 45 | 46 | it "creates a logger from another logger" do 47 | io = StringIO.new 48 | l = Rackamole::Logger.new( { :log_file => io, :logger_name => 'Mole' } ) 49 | child_l = l.for(Rack::Mole) 50 | 51 | child_l.info "This is a child log message" 52 | io.string.should =~ /This is a child log message/ 53 | io.string.should =~ /INFO/ 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/rackamole/store/log_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. spec_helper])) 2 | 3 | describe Rackamole::Store::Log do 4 | describe "#mole" do 5 | before( :each ) do 6 | @test_file = '/tmp/test_mole.log' 7 | File.delete( @test_file ) if File.exists?( @test_file ) 8 | 9 | @store = Rackamole::Store::Log.new( @test_file ) 10 | 11 | @args = BSON::OrderedHash.new 12 | @args[:type] = Rackamole.feature 13 | @args[:app_name] = "Test app" 14 | @args[:environment] = :test 15 | @args[:ip] = "1.1.1.1" 16 | @args[:browser] = BSON::OrderedHash.new 17 | @args[:browser][:name] = "Ibrowse" 18 | @args[:browser][:version] = "1.X" 19 | @args[:machine] = BSON::OrderedHash.new 20 | @args[:machine][:platform] = "Blee" 21 | @args[:machine][:os] = "Windoze" 22 | @args[:machine][:version] = "10.0" 23 | @args[:user_id] = 100 24 | @args[:user_name] = "Fernand" 25 | @args[:request_time] = 1.0 26 | @args[:url] = "http://test_me/" 27 | @args[:path] = "/fred" 28 | @args[:method] = 'GET' 29 | @args[:params] = { :blee => "duh".to_json } 30 | @args[:session] = { :fred => 10.to_json } 31 | end 32 | 33 | it "should mole a feature correctly" do 34 | @store.mole( @args ) 35 | results = File.read( @test_file ).gsub( /.* Mole \:\s/, '' ) 36 | expected = File.read( File.join( File.dirname(__FILE__), %w[.. .. expected_results mole_feature.log] ) ) 37 | expected.should == results 38 | end 39 | 40 | it "should mole an exception correctly" do 41 | @args[:type] = Rackamole.fault 42 | @args[:fault] = "Shiet" 43 | @args[:stack] = [ 'Oh snap!' ] 44 | @args[:ruby_version] = 'ruby 1.8.6 (2007-03-13 patchlevel 0) [i686-darwin8.10.1]' 45 | 46 | @store.mole( @args ) 47 | results = File.read( @test_file ).gsub( /.* Mole \:\s/, '' ) 48 | expected = File.read( File.join( File.dirname(__FILE__), %w[.. .. expected_results mole_exception.log] ) ) 49 | expected.should == results 50 | end 51 | 52 | it "should mole a performance issue correctly" do 53 | @args[:type] = Rackamole.perf 54 | @store.mole( @args ) 55 | results = File.read( @test_file ).gsub( /.* Mole \:\s/, '' ) 56 | expected = File.read( File.join( File.dirname(__FILE__), %w[.. .. expected_results mole_perf.log] ) ) 57 | expected.should == results 58 | end 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /lib/rackamole/utils/agent_detect.rb: -------------------------------------------------------------------------------- 1 | # This agent business is a cluster fuck. Won't even pretend detection will 2 | # be accurate. But at least an honest try... 3 | # BOZO !! Need to handle iphone, android, etc... 4 | module Rackamole::Utils 5 | class AgentDetect 6 | 7 | # browsers... 8 | def self.browsers() %w[Opera Firefox Chrome Safari MSIE]; end 9 | 10 | # Parses user agent to extract browser and machine info 11 | def self.parse( agent ) 12 | browsers.each do |browser| 13 | return extract( agent, browser ) if check?( agent, browser ) 14 | end 15 | defaults 16 | end 17 | 18 | # ========================================================================= 19 | private 20 | 21 | # Check for known browsers 22 | def self.check?( agent, browser ) 23 | agent.match( /#{browser}/ ) 24 | end 25 | 26 | # Pre populate info hash 27 | def self.defaults 28 | info = { :browser => {}, :machine => {} } 29 | %w[name version].each { |t| info[:browser][t.to_sym] = "N/A" } 30 | %w[platform os version local].each { |t| info[:machine][t.to_sym] = "N/A" } 31 | info 32 | end 33 | 34 | # Extract machine and browser info 35 | def self.extract( agent, browser ) 36 | @info = defaults 37 | begin 38 | extract_browser( agent, browser ) 39 | extract_platform( agent ) 40 | rescue => boom 41 | $stderr.puts "Unable to parse user agent `#{agent}" 42 | $stderr.puts boom 43 | boom.backtrace.each { |l| $stderr.puts l } 44 | end 45 | @info 46 | end 47 | 48 | # Extract browser and version 49 | def self.extract_browser( agent, browser ) 50 | @info[:browser][:name] = browser 51 | match = agent.match( /#{browser}[\/|\s]([\d|\.?]+)/ ) 52 | @info[:browser][:version] = match[1] if match and match[1] 53 | end 54 | 55 | # Extracts machine info 56 | def self.extract_platform( agent ) 57 | match = agent.match( /\((.*)\)/ ) 58 | return unless match and match[1] 59 | 60 | machine_info = match[1] 61 | tokens = machine_info.split( ";" ) 62 | unless tokens.empty? 63 | platform = tokens.shift.strip 64 | @info[:machine][:platform] = platform 65 | 66 | os_info = tokens.shift 67 | os_info = tokens.shift if os_info && os_info.match( /[MSIE|U]/ ) 68 | os = os_info.match( /(.+)\s([\w\d|\.]+)/ ) if os_info 69 | if os 70 | @info[:machine][:os] = os[1].strip if os[1] 71 | @info[:machine][:version] = os[2].strip if os[2] 72 | end 73 | end 74 | end 75 | end 76 | end -------------------------------------------------------------------------------- /lib/rackamole/stash/collector.rb: -------------------------------------------------------------------------------- 1 | module Rackamole::Stash 2 | # Caches perfs and faults. If either has been seen before update their 3 | # respective counts. This is used by the mole to track if a perf or exception 4 | # was previously recorded. 5 | class Collector 6 | 7 | attr_reader :app_id 8 | 9 | NEVER = -1 10 | 11 | def initialize( app_name, environment, expiration=24*60*60 ) 12 | @expiration = expiration 13 | @app_id = app_name + "_" + environment.to_s 14 | @faults = {} 15 | @perfs = {} 16 | end 17 | 18 | # Remove all entries that have expired 19 | def expire! 20 | expire_faults! 21 | expire_perfs! 22 | end 23 | 24 | # Delete all faults older than expiration 25 | def expire_faults! 26 | now = Time.now 27 | faults.each_pair do |stack, fault| 28 | if (now - fault.timestamp) >= expiration 29 | faults.delete( stack ) 30 | end 31 | end 32 | end 33 | 34 | # Delete all perfs older than expiration 35 | def expire_perfs! 36 | now = Time.now.utc 37 | perfs.each_pair do |path, perf| 38 | if (now - perf.timestamp) >= expiration 39 | perfs.delete( path ) 40 | end 41 | end 42 | end 43 | 44 | # Log or update fault if found... 45 | # Returns true if updated or false if created 46 | def stash_fault( path, stack, timestamp ) 47 | fault = find_fault( path, stack ) 48 | if fault 49 | fault.update( timestamp ) 50 | return true 51 | end 52 | fault = create_fault( path, stack, timestamp ) 53 | faults[stack] = fault 54 | false 55 | end 56 | 57 | # Log or update performance issue if found... 58 | # Returns true if updated or false if created 59 | def stash_perf( path, elapsed, timestamp ) 60 | perf = find_perf( path ) 61 | if perf 62 | perf.update( timestamp ) 63 | return true 64 | end 65 | perf = create_perf( path, elapsed, timestamp ) 66 | perfs[path] = perf 67 | false 68 | end 69 | 70 | # ========================================================================= 71 | private 72 | 73 | attr_reader :faults, :perfs, :expiration 74 | 75 | def create_fault( path, stack, timestamp ) 76 | faults[stack] = Rackamole::Stash::Fault.new( path, stack, timestamp ) 77 | end 78 | 79 | def create_perf( path, elapsed, timestamp ) 80 | perfs[path] = Rackamole::Stash::Perf.new( path, elapsed, timestamp ) 81 | end 82 | 83 | # Check if we've seen a similar fault on this application 84 | def find_fault( path, stack ) 85 | faults[stack] 86 | end 87 | 88 | # Check if we've seen this perf issue on this application 89 | def find_perf( path ) 90 | perfs[path] 91 | end 92 | end 93 | end -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | 0.0.1 - 0.1.0 2 | * Initial drop and bug fixes. Not that interesting... 3 | 4 | 0.1.1 5 | * Updated mongo store strategy 6 | * Tossed out mongo mapper. Trying to stay closer to the metal 7 | * Shortened col names for saving space. Unfortunately lame but necessary 8 | * Moved user info into separate collection. 9 | 10 | 0.2.0 11 | * YAML options - You can now load the rack component from a yaml file. This is 12 | very handy in framework where Rackamole options varies depending on the environment. 13 | Please refer to spec/test_configs/rackamole_test.yml for an example 14 | 15 | 0.2.2 16 | * NOTICE: Wrong docs and wrong configuration for mongo store initialization 17 | * Corrected docs and validation for mongo store init - changed :database to :db_name 18 | 19 | 0.2.3 20 | * Bug fixes 21 | * Fixed issue where session or param key falls under the format a.b.c which causes 22 | validation problems in mongoDB. Thanks Rafael! 23 | 24 | 0.2.4 25 | * Breaking off action_mailer dependencies. Now leveraging Pony for sending out email alerts. 26 | 27 | 0.2.5 28 | * Added support for excluding certain params from request or session. 29 | See the params_excludes and session_excludes options. 30 | 31 | 0.2.6 32 | * Added logging for response information such as status, headers and body. Body is excluded by default 33 | * Added options for excluding certain mole information for being logged see mole_excludes option. 34 | * Eliminated unwise defaults. From now, the option :app_name must be specified when initializing. 35 | The option :db_name must also be present if the mongo store is used. 36 | Keep in mind for rackamole to work with wackamole your db name must follow: mole_{app_name}_{environment}_mdb. 37 | 38 | 0.2.7 39 | * Bug fixes and cleanup 40 | * Added user_agent parsing. now extracting os/platform/version as well as browser type and version. 41 | 42 | 0.2.8 43 | * Formatted email alerts so they are a little more readable 44 | 45 | 0.2.9 46 | * Fixed perf email headline 47 | 48 | 0.3.0 49 | * Fixed issue with rails and params not being correctly recorded 50 | 51 | 0.3.1 52 | * Bugs and cleanups 53 | 54 | 0.3.2 55 | * Fix issue with excluded_path being nil. 56 | * Changed default excluded_path 57 | * Added perf_excludes option to exempt alerts from being sent for known slower requests 58 | 59 | 0.3.3 60 | * Added support for rackamole store authentication 61 | * Fixed issue to enforce regexp when excluded path are specified as strings. 62 | 63 | 0.3.4 64 | * Bugs and clean up 65 | 66 | 0.3.5 67 | * Bugs and clean up 68 | 69 | 0.3.6 70 | * Added growl notification alerts 71 | 72 | 0.3.7 Updates to use the latest mongo ruby gem 73 | NOTE: Some api changed in this version of the mongo ruby driver and hosed OrderedHash refs. 74 | 75 | 0.3.8 Updates for ruby 1.9 compatibility 76 | 77 | 0.3.9 Merged Tatsuya - nested filter parameters 78 | 79 | 0.4.0 80 | * Updated bones version 81 | * Added bundler Gemfile 82 | * Fixed bug parsing agent 83 | 84 | 0.4.1 85 | * Updates for rails 3.0.8 -------------------------------------------------------------------------------- /spec/rackamole/alert/growl_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. spec_helper])) 2 | require 'chronic' 3 | 4 | describe Rackamole::Alert::Growl do 5 | before( :each ) do 6 | @recipients = [ {:ip => '1.1.1.1', :password => 'blee' } ] 7 | @alert = Rackamole::Alert::Growl.new( nil, :growl => { :to => @recipients } ) 8 | end 9 | 10 | describe '#display_feature' do 11 | it "should display a rails feature correctly" do 12 | @alert.send( :display_feature, :route_info => { :controller => 'fred', :action => 'blee'} ).should == "fred#blee" 13 | end 14 | 15 | it "should display a path feature for other rack framword" do 16 | @alert.send( :display_feature, :path => '/fred/blee' ).should == "/fred/blee" 17 | end 18 | end 19 | 20 | describe '#send_alert' do 21 | before( :each ) do 22 | @args = BSON::OrderedHash.new 23 | @args[:type] = Rackamole.feature 24 | @args[:app_name] = 'Test' 25 | @args[:host] = 'Fred' 26 | @args[:user_name] = 'Fernand' 27 | @args[:path] = '/blee/fred' 28 | @args[:created_at] = Chronic.parse( "2009/11/19" ) 29 | end 30 | 31 | it "should growl a feature alert using class method correctly" do 32 | growl = mock( Rackamole::Alert::Growl ) 33 | client = Growl.stub!( :new ) 34 | 35 | Rackamole::Alert::Growl.should_receive( :new ).with( nil, @recipients ).once.and_return( growl ) 36 | growl.should_receive( :send_alert ).with( @args ).once.and_return( "yeah" ) 37 | 38 | Rackamole::Alert::Growl.deliver_alert( nil, { :growl => { :to => @recipients } }, @args ) 39 | end 40 | 41 | it "should growl a feature alert correctly" do 42 | client = stub( Growl ) 43 | 44 | @alert.should_receive( :growl ).once.and_return( client ) 45 | client.should_receive( :notify ).once 46 | 47 | @alert.send_alert( @args ) 48 | end 49 | 50 | it "should growl a perf alert correctly" do 51 | @args[:type] = Rackamole.perf 52 | @args[:request_time] = 10.0 53 | 54 | client = stub( Growl ) 55 | 56 | @alert.should_receive( :growl ).once.and_return( client ) 57 | client.should_receive( :notify ).once 58 | 59 | @alert.send_alert( @args ) 60 | end 61 | 62 | it "should twitt a perf alert correctly" do 63 | @args[:type] = Rackamole.fault 64 | @args[:fault] = 'Oh snap!' 65 | 66 | client = stub( Growl ) 67 | 68 | @alert.should_receive( :growl ).once.and_return( client ) 69 | client.should_receive( :notify ).once 70 | 71 | @alert.send_alert( @args ) 72 | end 73 | end 74 | 75 | describe "#format_time" do 76 | it "should format a request time correctly" do 77 | @alert.send( :format_time, 12.1234455 ).should == 12.12 78 | end 79 | end 80 | 81 | describe "#format_host" do 82 | it "should format a host with domain name correctly" do 83 | @alert.send( :format_host, 'blee@acme.com' ).should == 'blee' 84 | end 85 | 86 | it "should deal with ip host" do 87 | @alert.send( :format_host, '1.1.1.1' ).should == '1.1.1.1' 88 | end 89 | 90 | it "should deal with aliases" do 91 | @alert.send( :format_host, 'fred' ).should == 'fred' 92 | end 93 | end 94 | 95 | end -------------------------------------------------------------------------------- /spec/rackamole/alert/twitt_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. spec_helper])) 2 | require 'chronic' 3 | 4 | describe Rackamole::Alert::Twitt do 5 | before( :each ) do 6 | @alert = Rackamole::Alert::Twitt.new( nil, 'fernand', 'blee' ) 7 | end 8 | 9 | it "should truncate a message correctly" do 10 | @alert.send( :truncate, "a"*141 ).should == "a"*137 + '...' 11 | end 12 | 13 | describe '#display_feature' do 14 | it "should display a rails feature correctly" do 15 | @alert.send( :display_feature, :route_info => { :controller => 'fred', :action => 'blee'} ).should == "fred#blee" 16 | end 17 | 18 | it "should display a path feature for other rack framword" do 19 | @alert.send( :display_feature, :path => '/fred/blee' ).should == "/fred/blee" 20 | end 21 | end 22 | 23 | describe '#send_alert' do 24 | before( :each ) do 25 | @args = BSON::OrderedHash.new 26 | @args[:type] = Rackamole.feature 27 | @args[:app_name] = 'Test' 28 | @args[:host] = 'Fred' 29 | @args[:user_name] = 'Fernand' 30 | @args[:path] = '/blee/fred' 31 | @args[:created_at] = Chronic.parse( "2009/11/19" ) 32 | end 33 | 34 | it "should twitt a feature alert using class method correctly" do 35 | twitt = mock( Rackamole::Alert::Twitt ) 36 | client = Twitter::Client.stub!( :new ) 37 | 38 | Rackamole::Alert::Twitt.should_receive( :new ).with( nil, 'blee', 'duh').once.and_return( twitt ) 39 | # client.should_receive( :new ).once.and_return( client ) 40 | twitt.should_receive( :send_alert ).with( @args ).once.and_return( "yeah" ) 41 | 42 | Rackamole::Alert::Twitt.deliver_alert( nil, { :twitter => { :username => "blee", :password => "duh" } }, @args ) 43 | end 44 | 45 | it "should twitt a feature alert correctly" do 46 | client = stub( Twitter::Client ) 47 | 48 | @alert.should_receive( :twitt ).once.and_return( client ) 49 | # client.should_receive( :new ).exactly(1).with( 'fernand', 'blee' ) 50 | client.should_receive( :status ).once 51 | 52 | @alert.send_alert( @args ).should == "[Feature] Test on Fred - Fernand\n/blee/fred - 12:00:00" 53 | end 54 | 55 | it "should twitt a perf alert correctly" do 56 | @args[:type] = Rackamole.perf 57 | @args[:request_time] = 10.0 58 | 59 | client = stub( Twitter::Client ) 60 | 61 | @alert.should_receive( :twitt ).once.and_return( client ) 62 | client.should_receive( :status ).once 63 | 64 | @alert.send_alert( @args ).should == "[Perf] Test on Fred - Fernand\n/blee/fred\n10.0 secs - 12:00:00" 65 | end 66 | 67 | it "should twitt a perf alert correctly" do 68 | @args[:type] = Rackamole.fault 69 | @args[:fault] = 'Oh snap!' 70 | 71 | client = stub( Twitter::Client ) 72 | 73 | @alert.should_receive( :twitt ).once.and_return( client ) 74 | client.should_receive( :status ).once 75 | 76 | @alert.send_alert( @args ).should == "[Fault] Test on Fred - Fernand\n/blee/fred\nOh snap! - 12:00:00" 77 | end 78 | end 79 | 80 | describe "#format_time" do 81 | it "should format a request time correctly" do 82 | @alert.send( :format_time, 12.1234455 ).should == 12.12 83 | end 84 | end 85 | 86 | describe "#format_host" do 87 | it "should format a host with domain name correctly" do 88 | @alert.send( :format_host, 'blee@acme.com' ).should == 'blee' 89 | end 90 | 91 | it "should deal with ip host" do 92 | @alert.send( :format_host, '1.1.1.1' ).should == '1.1.1.1' 93 | end 94 | 95 | it "should deal with aliases" do 96 | @alert.send( :format_host, 'fred' ).should == 'fred' 97 | end 98 | end 99 | 100 | end -------------------------------------------------------------------------------- /lib/rackamole/alert/twitt.rb: -------------------------------------------------------------------------------- 1 | require 'twitter' 2 | 3 | module Rackamole::Alert 4 | # Leverage twitter as a notification client. You can setup a private twitter account 5 | # and have your moled app twitt exception/perf alerts... 6 | class Twitt 7 | 8 | # Twitt an alert 9 | def self.deliver_alert( logger, options, attrs ) 10 | @twitt ||= Twitt.new( logger, options[:twitter][:username], options[:twitter][:password] ) 11 | @twitt.send_alert( attrs ) 12 | end 13 | 14 | # This class is used to send out moled twitter notification. This feature is enabled 15 | # by setting the :twitter option on the Rack::Mole. When a moled 16 | # feature comes around it will be twitted on your configured account. This allow your 17 | # app to twitt about it's status and issues. Currently there are no provisions to throttle 18 | # the twitts, hence sending out twitt notifications of every moled features would not be 19 | # a very good idea. Whereas sending twitts when your application bogs down or throws exception, 20 | # might be more appropriate. Further work will take place to throttle these events... 21 | # Creating a private twitter account and asking folks in your group to follow might be a 22 | # nice alternative to email. 23 | # 24 | # NOTE: This is just an alert mechanism. All moled events will be either logged or persisted in the db 25 | # regardless. 26 | # 27 | # === Params: 28 | # username :: The name on the twitter account 29 | # password :: The password of your twitter account 30 | # logger :: Instance of the rackamole logger 31 | def initialize( logger, username, password ) 32 | raise "You must specify your twitter account credentials" unless username or password 33 | @username = username 34 | @password = password 35 | @logger = logger 36 | end 37 | 38 | # Send out a twitt notification based of the watched features. A short message will be blasted to your twitter 39 | # account based on information reported by the mole. The twitt will be automatically truncated 40 | # to 140 chars. 41 | # 42 | # === Params: 43 | # args :: The moled info for a given feature. 44 | # 45 | def send_alert( args ) 46 | twitt_msg = "#{args[:app_name]} on #{format_host(args[:host])} - #{args[:user_name]}\n#{display_feature(args)}" 47 | twitt_msg = case args[:type] 48 | when Rackamole.feature 49 | "[Feature] #{twitt_msg}" 50 | when Rackamole.perf 51 | "[Perf] #{twitt_msg}\n#{format_time(args[:request_time])} secs" 52 | when Rackamole.fault 53 | "[Fault] #{twitt_msg}\n#{args[:fault]}" 54 | end 55 | if twitt_msg 56 | twitt_msg += " - #{args[:created_at].strftime( "%H:%M:%S")}" 57 | twitt.status( :post, truncate( twitt_msg ) ) 58 | end 59 | twitt_msg 60 | rescue => boom 61 | logger.error "Rackamole twitt alert failed with error `#{boom}" 62 | end 63 | 64 | # ========================================================================= 65 | private 66 | 67 | attr_reader :logger, :username, :password #:nodoc: 68 | 69 | # Fetch twitter connection... 70 | def twitt 71 | @twitt ||= ::Twitter::Client.new( :login => username, :password => password ) 72 | end 73 | 74 | # Display controller/action or path depending on frmk used... 75 | def display_feature( args ) 76 | return args[:path] unless args[:route_info] 77 | "#{args[:route_info][:controller]}##{args[:route_info][:action]}" 78 | end 79 | 80 | # Format host ie fred@blee.com => fred 81 | def format_host( host ) 82 | return host.gsub( /@.+/, '' ) if host =~ /@/ 83 | host 84 | end 85 | 86 | # Format precision on request time 87 | def format_time( time ) 88 | ("%4.2f" % time).to_f 89 | end 90 | 91 | # Truncate for twitt max size 92 | # BOZO !! This will be hosed if not 1.9 for multibyte chars 93 | def truncate(text, length = 140, truncate_string = "...") 94 | return "" if text.nil? 95 | l = length - truncate_string.size 96 | text.size > length ? (text[0...l] + truncate_string).to_s : text 97 | end 98 | end 99 | end -------------------------------------------------------------------------------- /lib/rackamole/alert/growl.rb: -------------------------------------------------------------------------------- 1 | require 'ruby-growl' 2 | 3 | module Rackamole::Alert 4 | # Leverages growl as a notification client. 5 | class Growl 6 | 7 | # Twitt an alert 8 | def self.deliver_alert( logger, options, attrs ) 9 | @growl ||= Growl.new( logger, options[:growl][:to] ) 10 | @growl.send_alert( attrs ) 11 | end 12 | 13 | # Send a growl notification for particular moled feature. A growl will 14 | # be sent based on the configuration :growl defined on Rack::Mole component 15 | # This option must specify the to ip addresses and the conditions 16 | # that will trigger a growl defined by alert_on for the type of 17 | # moled features to track via email. The notification will be sent via UDP, 18 | # so you will need to make sure it is properly configured for your domain. 19 | # 20 | # NOTE: This is just a notification mechanism. All moled event will be either logged 21 | # or persisted in the db regardless. 22 | # 23 | # === Parameters: 24 | # options :: Mole options. The :growl key must contains :to addresses. 25 | # args :: The gathered information from the mole. 26 | def initialize( logger, recipients ) 27 | raise "You must specify your growl :to addresses" unless recipients 28 | @recipients = recipients 29 | @logger = logger 30 | @growls = {} 31 | end 32 | 33 | # Send out a growl notification based of the watched features. A short message will be blasted to your growl 34 | # client based on information reported by the mole. 35 | # === Params: 36 | # args :: The moled info for a given feature. 37 | def send_alert( args ) 38 | recipients.each do |recipient| 39 | buff = "#{args[:user_name]}:#{display_feature(args)}" 40 | title = "#{args[:app_name]}(#{args[:environment]})" 41 | case args[:type] 42 | when Rackamole.feature 43 | type = "Feature" 44 | title = "[Feat] #{title}" 45 | msg = buff 46 | priority = -2 47 | sticky = false 48 | when Rackamole.perf 49 | type = "Perf" 50 | title = "[Perf] #{title}" 51 | msg = "#{buff} #{format_time(args[:request_time])} secs" 52 | priority = 2 53 | sticky = true 54 | when Rackamole.fault 55 | type = "Fault" 56 | title = "[Fault] #{title}" 57 | msg = "#{buff}\n#{args[:fault]}" 58 | priority = 2 59 | sticky = true 60 | end 61 | growl( recipient ).notify( type, title, msg, priority, sticky ) 62 | end 63 | rescue => boom 64 | logger.error "Rackamole growl alert failed with error `#{boom}" 65 | end 66 | 67 | # ========================================================================= 68 | private 69 | 70 | attr_reader :logger, :recipients #:nodoc: 71 | 72 | # Fetch or create growl application... 73 | def growl( recipient ) 74 | return @growls[recipient[:ip]] if @growls[recipient[:ip]] 75 | growl = ::Growl.new( recipient[:ip], 'rackamole', %w(Feature Perf Fault), nil, recipient[:password] ) 76 | @growls[recipient[:ip]] = growl 77 | growl 78 | end 79 | 80 | # Display controller/action or path depending on frmk used... 81 | def display_feature( args ) 82 | return args[:path] unless args[:route_info] 83 | "#{args[:route_info][:controller]}##{args[:route_info][:action]}" 84 | end 85 | 86 | # Format host ie fred@blee.com => fred 87 | def format_host( host ) 88 | return host.gsub( /@.+/, '' ) if host =~ /@/ 89 | host 90 | end 91 | 92 | # Format precision on request time 93 | def format_time( time ) 94 | ("%4.2f" % time).to_f 95 | end 96 | 97 | # Truncate for twitt max size 98 | # BOZO !! This will be hosed if not 1.9 for multibyte chars 99 | def truncate(text, length = 140, truncate_string = "...") 100 | return "" if text.nil? 101 | l = length - truncate_string.size 102 | text.size > length ? (text[0...l] + truncate_string).to_s : text 103 | end 104 | end 105 | end -------------------------------------------------------------------------------- /lib/rackamole/alert/emole.rb: -------------------------------------------------------------------------------- 1 | require 'mail' 2 | require 'erubis' 3 | 4 | module Rackamole::Alert 5 | class Emole 6 | 7 | # retrieves erb template dir 8 | def self.template_root() @template_root ||= File.join( File.dirname(__FILE__), %w[templates] ); end 9 | 10 | # Send an email notification for particular moled feature. An email will 11 | # be sent based on the configuration :email defined on the 12 | # Rack::Mole component. This option must specify the to and from addresses and the conditions 13 | # that will trigger the email defined by alert_on for the type of 14 | # moled features to track via email. The notification will be sent via Mail, 15 | # so you will need to make sure it is properly configured for your domain. 16 | # NOTE: This is just a notification mechanism. All moled event will be either logged 17 | # or persisted in the db regardless. 18 | # 19 | # === Parameters: 20 | # options :: Mole options. The :email key minimaly contains :from for the from address. Must be a valid domain. 21 | # :: And a :to, n array of email addresses for recipients to be notified. 22 | # args :: The gathered information from the mole. 23 | # 24 | def self.deliver_alert( logger, options, args ) 25 | @options = options 26 | @args = args 27 | tmpl = File.join( template_root, %w[alert.erb] ) 28 | template = Erubis::Eruby.new( IO.read( tmpl ), :trim => true ) 29 | body = template.result( binding ) 30 | timing = request_time( args ) if args[:type] == Rackamole.perf 31 | subject = "Rackamole <#{alert_type( args )}>#{timing ? " #{timing} " : ' '}on #{args[:app_name]}.#{args[:host]} for user #{args[:user_name]}" 32 | 33 | mail = Mail.new do 34 | from options[:email][:from] 35 | to options[:email][:to] 36 | subject subject 37 | body body 38 | end 39 | mail.deliver! 40 | mail 41 | rescue => boom 42 | logger.error( "Rackamole email alert failed with error `#{boom}" ) 43 | boom.backtrace.each { |l| logger.error l } 44 | end 45 | 46 | def self.section( title ) 47 | buff = [] 48 | buff << "-"*40 49 | buff << "o #{title.capitalize}\n" 50 | buff << self.send( title.downcase ) 51 | buff << "\n" 52 | buff.join( "\n" ) 53 | end 54 | 55 | # ========================================================================= 56 | private 57 | 58 | def self.args 59 | @args 60 | end 61 | 62 | def self.humanize( key ) 63 | key 64 | end 65 | 66 | def self.feature_type 67 | case args[:type] 68 | when Rackamole.feature 69 | "Feature" 70 | when Rackamole.perf 71 | "Performance" 72 | when Rackamole.fault 73 | "Fault" 74 | end 75 | end 76 | 77 | # Format args and spit out into buffer 78 | def self.spew( key, silent=false ) 79 | buff = [] 80 | value = args[key] 81 | 82 | value = request_time( args ) if key == :request_time 83 | 84 | _spew( buff, '--', (silent ? '' : ' '), key, value, silent ) 85 | buff.join( "\n" ) 86 | end 87 | 88 | def self._spew( buff, sep, indent, key, value, silent ) 89 | if value.is_a?( Hash ) 90 | buff << "#{indent}#{humanize( key )}:" unless silent 91 | value.each_pair{ |k,v| _spew( buff, sep, indent+" ", k, v, false ) } 92 | elsif value.is_a?( Array ) 93 | buff << "#{indent}#{humanize( key )}:" unless silent 94 | value.each { |s| _spew( buff, sep, indent+" ", '', s, false ) } 95 | else 96 | buff << "#{indent}#{humanize( key )}: #{value}" 97 | end 98 | end 99 | 100 | # What just happened? 101 | def self.what 102 | buff = [] 103 | case args[:type] 104 | when Rackamole.fault 105 | buff << spew( :fault ) << spew( :stack ) + "\n" 106 | when Rackamole.perf 107 | buff << "#{spew( :request_time )} [#{@options[:perf_threshold]}]" 108 | end 109 | buff << spew( :user_name) << spew( :url ) << spew( :path ) << spew( :status ) 110 | buff << spew( :method ) 111 | buff << spew( :request_time ) unless args[:type] == Rackamole.perf 112 | buff << spew( :ip ) 113 | buff.join( "\n" ) 114 | end 115 | def self.server() [ spew( :host ), spew( :software ), spew( :ruby_version ) ]; end 116 | def self.client() [ spew( :machine, true ) ]; end 117 | def self.params() [ spew( :params, true ) ]; end 118 | def self.session() [ spew( :session, true ) ]; end 119 | def self.browser() [ spew( :browser, true ) ]; end 120 | def self.headers() [ spew( :headers, true ) ]; end 121 | 122 | # Dump request time if any... 123 | def self.request_time( args ) 124 | return '' unless args[:request_time] 125 | "%1.2f" % args[:request_time] 126 | end 127 | 128 | # Identify the type of alert... 129 | def self.alert_type( args ) 130 | case args[:type] 131 | when Rackamole.feature 132 | "Feature" 133 | when Rackamole.perf 134 | "Performance" 135 | when Rackamole.fault 136 | "Fault" 137 | end 138 | end 139 | end 140 | end -------------------------------------------------------------------------------- /lib/rackamole/logger.rb: -------------------------------------------------------------------------------- 1 | require 'logging' 2 | require 'forwardable' 3 | 4 | module Rackamole 5 | class Logger 6 | class ConfigurationError < StandardError ; end #:nodoc: 7 | 8 | attr_reader :log #:nodoc: 9 | 10 | extend Forwardable 11 | def_delegators :@log, :debug, :warn, :info, :error, :fatal 12 | def_delegators :@log, :level=, :level 13 | def_delegators :@log, :debug?, :warn?, :info?, :error?, :fatal? 14 | def_delegators :@log, :add, :clear_appenders 15 | 16 | # there are more options here than are typically utilized for the 17 | # logger, they are made available if someone wants to utilize 18 | # them directly. 19 | # 20 | def self.default_options #:nodoc: 21 | @default_options ||= { 22 | 23 | # log event layout pattern 24 | # YYYY-MM-DDTHH:MM:SS 12345 LEVEL LoggerName : The Log message 25 | # 26 | :layout_pattern => '%d %5p %5l %c : %m\n'.freeze , 27 | :logger_name => 'Mole' , 28 | :additive => true , 29 | 30 | # log file configuration options 31 | # age -> how often to rotate the logs if a file is used 32 | # keep_count -> how many of the log files to keep 33 | :log_level => :info , 34 | :log_file => $stdout , 35 | :log_file_age => 'daily'.freeze , 36 | :log_file_keep_count => 7 , 37 | 38 | # email logging options 39 | # buffsize -> number of log events used as a threshold to send an 40 | # email. If that is not reached before the program 41 | # exists then the at_exit handler for logging will flush 42 | # the log to smtp. 43 | :email_alerts_to => nil , 44 | :email_alert_level => :error , 45 | :email_alert_server => nil , 46 | :email_alert_buffsize => 200 , 47 | } 48 | end 49 | 50 | # Creates a logger for mole usage by leveraging the most excellent logging gem. 51 | # This provides for a semi persistent storage for mole information, typically set up 52 | # for the console or a file. By default moled features will be sent out to the console. 53 | # Alternatively you can store the moled info to a file. 54 | # 55 | def initialize( opts = {} ) 56 | @options = ::Rackamole::Logger.default_options.merge(opts) 57 | @log = ::Logging::Logger[@options[:logger_name]] 58 | @layout = ::Logging::Layouts::Pattern.new( { :pattern => @options[:layout_pattern] } ) 59 | 60 | # add appenders explicitly, since this logger may already be defined and 61 | # already have loggers 62 | @appenders = [] 63 | @appenders << log_file_appender if @options[:log_file] 64 | @appenders << email_appender if @options[:email_alerts_to] 65 | 66 | @log.appenders = @appenders 67 | @log.level = @options[:log_level] 68 | @log.additive = @options[:additive] 69 | end 70 | 71 | # File appender, this is either an IO appender or a RollingFile appender 72 | # depending on if an IO object or a String is passed in. 73 | # 74 | # a configuration error is raised in any other circumstance 75 | def log_file_appender #:nodoc: 76 | appender_name = "#{@log.name}-log_file_appender" 77 | if String === @options[:log_file] then 78 | ::Logging::Appenders::RollingFile.new( appender_name, 79 | { :layout => @layout, 80 | :filename => @options[:log_file], 81 | :age => @options[:log_file_age], 82 | :keep => @options[:log_file_keep_count], 83 | :safe => true # make sure log files are rolled using lockfile 84 | }) 85 | elsif @options[:log_file].respond_to?(:print) then 86 | ::Logging::Appenders::IO.new( appender_name, @options[:log_file], :layout => @layout ) 87 | else 88 | raise ConfigurationError, "Invalid :log_file option [#{@options[:log_file].inspect}]" 89 | end 90 | end 91 | 92 | # an email appender that uses :email_alerts_to option to send emails to. 93 | # :email_alerts_to can either be a singe email address as a string or an 94 | # Array of email addresses. Any other option for :email_alerts_to is 95 | # invalid and raises an error. 96 | # 97 | def email_appender #:nodoc: 98 | email_alerts_to = [ @options[:email_alerts_to] ].flatten.reject { |x| x == nil } 99 | raise ConfigurationError, "Invalid :email_alerts_to option [#{@options[:email_alerts_to].inspect}]" unless email_alerts_to.size > 0 100 | ::Logging::Appenders::Email.new("#{@log.name}-email_appender", 101 | { 102 | :level => @options[:email_alert_level], 103 | :layout => @layout, 104 | :from => "#{@log.name}", 105 | :to => "#{email_alerts_to.join(', ')}", 106 | :subject => "Logging Alert from #{@log.name} on #{ENV['HOSTNAME']}", 107 | :server => @options[:email_alert_server], 108 | :buffsize => @options[:email_alert_buffsize], # lines 109 | }) 110 | end 111 | 112 | # create a new logger thi logger has no options when it is created although 113 | # more can be added with the logger instance that is returned. The 114 | # appenders of the current instance of Logger will be set on the new 115 | # logger and the options of the current logger will be applied 116 | def for( arg ) #:nodoc: 117 | new_logger = ::Logging::Logger[arg] 118 | new_logger.level = @options[:level] 119 | new_logger.additive = @options[:additive] 120 | new_logger.appenders = @appenders 121 | return new_logger 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | == RackAmole 2 | 3 | Observe your web applications in the wild! 4 | 5 | == DESCRIPTION 6 | 7 | The MOle is a rack application that monitors user interactions with your web site. We are not 8 | talking about counting page hits here. The MOle tracks all the information available to capture 9 | the essence of a user interaction with your application. Using the MOle, you are able to see 10 | which feature is a hit or a bust. As an added bonus, the MOle also track performance and exceptions 11 | that might have escaped your test suites or alpha env. To boot your managers will love you for it! 12 | 13 | Whether you are releasing a new application or improving on an old one, it is always a good thing 14 | to know if anyone is using your application and if they are, how they are using it. 15 | What features are your users most fond of and which features find their way into the abyss? 16 | You will be able to rapidly assess whether or not your application is a hit and if 17 | your coolest features are thought as such by your users. You will be able to elegantly record user 18 | interactions and leverage these findings for the next iteration of your application. 19 | 20 | == PROJECT INFORMATION 21 | 22 | Developer: Fernand Galiana 23 | Blog: http://www.liquidrail.com 24 | Site: http://rackamole.com 25 | Twitter: http://twitter.com/rackamole 26 | Forum: http://groups.google.com/group/rackamole 27 | Git: git://github.com/derailed/rackamole.git 28 | 29 | == FEATURES 30 | 31 | Monitors any rack based framework such as Rails and Sinatra 32 | Captures the essence of the request as well as user information 33 | Tracks performance issues based on your latency threshold 34 | Tracks exceptions that might occurred during a request 35 | 36 | == REQUIREMENTS 37 | 38 | Logging 39 | Hitimes 40 | mongo + mongo_ext 41 | Chronic 42 | Erubis 43 | Twitter4r 44 | Mail 45 | Growl 46 | 47 | == INSTALL 48 | 49 | sudo gem install rackamole 50 | 51 | == USAGE 52 | 53 | === Rails applications 54 | 55 | Edit your environments ruby files and add the following lines: 56 | 57 | config.middleware.use Rack::Mole, { :app_name => "My Cool App", :user_key => :user_name } 58 | 59 | This instructs the mole to start logging information to the console and look for the user name 60 | in the session using the :user_name key. In order to associate an action with a logged in user you 61 | must set a session env variable, in this case we use user_name. There are other options available, 62 | please take a look at the docs for more information. 63 | 64 | === Sinatra Applications 65 | 66 | Add the following lines in the config section and smoke it... 67 | 68 | require 'rackamole' 69 | configure do 70 | use Rack::Mole, { :app_name => "My Sinatra App", :user_key => :user_name } 71 | end 72 | 73 | This assumes that you have session enabled to identify a user if not the mole will log the user 74 | as 'Unknown' 75 | 76 | === Notables 77 | 78 | Rackamole also comes with an option to specify a yaml config file to initialize the various settings. 79 | This comes in very handy when you need to specify different options depending on the environment you 80 | are operating in. Please see the spec/test_configs/rackamole_test.yml for an example. 81 | 82 | === Storing moled information 83 | 84 | Rackamole currently comes with a single storage strategy. More will come in the near future, but 85 | currently we are using MongoDb as our default storage. The idea here is to create a database for 86 | a given moled app per environment. For instance, for application 'Fred', you will need to use a 87 | separate store for Fred running in alpha mode and Fred running in production mode. 88 | 89 | In order to use a store, you will need to pass in the :store option. There currently 2 store 90 | types a logger and a mongo adapter. By default the store is set to log moled information to the console. 91 | To change to a mongo store simply add the following options: 92 | 93 | use Rack::Mole, { :app_name => "Fred", :store => Rackamole::Store::MongoDb.new( :db_name => 'mole_fred_alpha_mdb' ) } 94 | 95 | This expect a local mongo instance to be running on the default port. You can change the 96 | location by adding :host and :port options. 97 | 98 | NOTE: If you intend to use Wackamole please use the following mongo database naming convention 99 | 100 | mole_{app_name}_{environment}_mdb 101 | 102 | NOTE: Rackamole also provides for preventing certain sensitive params from being logged. You can specify 103 | param_excludes or session_excludes as array of symbols to exclude specific request or session params. 104 | 105 | === Alerting 106 | 107 | Rackamole provides 3 different kind of alerting mechanisms: twitter, email, growl 108 | Please see docs for the various configuration settings. 109 | 110 | For example to setup email alerts, add the following lines in your rackamole config file. 111 | 112 | # Email 113 | email: &email 114 | :from: 'rackamole@acme.com' 115 | :to: 116 | - 'fernand@acme.com' 117 | - 'molley@acme.com' 118 | :alert_on: 119 | - <%=Rackamole.perf%> 120 | - <%=Rackamole.fault%> 121 | 122 | Then 123 | 124 | # => Dev 125 | development: 126 | :app_name: Killer App 127 | :user_key: :user_name 128 | :email: *email 129 | 130 | This will setup email alerts when rackamole detect performance or uncaught exceptions 131 | 132 | == LICENSE: 133 | 134 | (The MIT License) 135 | 136 | Copyright (c) 2009 137 | 138 | Permission is hereby granted, free of charge, to any person obtaining 139 | a copy of this software and associated documentation files (the 140 | 'Software'), to deal in the Software without restriction, including 141 | without limitation the rights to use, copy, modify, merge, publish, 142 | distribute, sublicense, and/or sell copies of the Software, and to 143 | permit persons to whom the Software is furnished to do so, subject to 144 | the following conditions: 145 | 146 | The above copyright notice and this permission notice shall be 147 | included in all copies or substantial portions of the Software. 148 | 149 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 150 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 151 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 152 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 153 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 154 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 155 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /spec/rackamole/utils/agent_detect_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. spec_helper])) 2 | 3 | describe Rackamole::Utils::AgentDetect do 4 | 5 | describe "os" do 6 | it "should detect the os and version correctly" do 7 | agents = [ 8 | # "Opera/8.65 (X11; Linux i686; U; ru)", 9 | # "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) Opera 8.65 [en]", 10 | # "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.7) Gecko/2009021910 Firefox/1.5.0.12 (.NET CLR 3.5.30729)", 11 | # "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.0.6) Gecko/2009011912 Firefox/1.5.0.12 Ubiquity/0.1.5", 12 | # "Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.0.12) Gecko/20080326 CentOS/1.5.0.12-14.el5.centos Firefox/1.5.0.12", 13 | # "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.24 Safari/532.0", 14 | # "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.24 Safari/532.0", 15 | # "Mozilla/5.0 (iPod; U; CPU like Mac OS X; fr) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/4A102 Safari/522.12", 16 | # "Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/522.12", 17 | # "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", 18 | # "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; SV1; CPT-IE401SP1; .NET CLR 1.0.3705; .NET CLR 1.1.4322; InfoPath.1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30)", 19 | # "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 1.1.4322; InfoPath.2)", 20 | # "Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)", 21 | # "Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.6.30 Version/10.61", 22 | "Safari/6533.17.8 CFNetwork/454.9.7 Darwin/10.4.0 (i386) (iMac10%2C1)", 23 | # "Mozilla/5.0 (SnapPreviewBot) Gecko/20061206 Firefox/1.5.0.9", 24 | # "StackRambler/2.0 (MSIE incompatible)" 25 | ] 26 | expectations = [ 27 | { 28 | :browser => { :name => "Safari", :version => "6533.17.8" }, 29 | :machine => { :platform => "i386", :os => "Darwin", :version => "10.4.0", :local => "N/A" } 30 | }, 31 | { 32 | :browser => { :name => "Opera", :version => "8.65" }, 33 | :machine => { :platform => "X11", :os => "Linux", :version => "i686", :local => "N/A" } 34 | }, 35 | { 36 | :browser => { :name => "Opera", :version => "8.65" }, 37 | :machine => { :platform => "compatible", :os => "Windows NT", :version => "5.1", :local => "N/A" } 38 | }, 39 | { 40 | :browser => { :name => "Firefox", :version => "1.5.0.12" }, 41 | :machine => { :platform => "Windows", :os => "Windows NT", :version => "6.0", :local => "en-US" } 42 | }, 43 | { 44 | :browser => { :name => "Firefox", :version => "1.5.0.12" }, 45 | :machine => { :platform => "Macintosh", :os => "Intel Mac OS X", :version => "10.5", :local => "en-US" } 46 | }, 47 | { 48 | :browser => { :name => "Firefox", :version => "1.5.0.12" }, 49 | :machine => { :platform => "X11", :os => "Linux", :version => "i686", :local => "en-US" } 50 | }, 51 | { 52 | :browser => { :name => "Chrome", :version => "3.0.195.24" }, 53 | :machine => { :platform => "Windows", :os => "Windows NT", :version => "5.1", :local => "en-US" } 54 | }, 55 | { 56 | :browser => { :name => "Chrome", :version => "3.0.195.24" }, 57 | :machine => { :platform => "Macintosh", :os => "Intel Mac OS X", :version => "10_5_6", :local => "en-US" } 58 | }, 59 | { 60 | :browser => { :name => "Safari", :version => "522.12" }, 61 | :machine => { :platform => "iPod", :os => "CPU like Mac OS", :version => "X", :local => "fr" } 62 | }, 63 | { 64 | :browser => { :name => "Safari", :version => "522.12" }, 65 | :machine => { :platform => "iPhone", :os => "CPU like Mac OS", :version => "X", :local => "en" } 66 | }, 67 | { 68 | :browser => { :name => "MSIE", :version => "8.0" }, 69 | :machine => { :platform => "compatible", :os => "Windows NT", :version => "5.1", :local => "N/A" } 70 | }, 71 | { 72 | :browser => { :name => "MSIE", :version => "7.0" }, 73 | :machine => { :platform => "compatible", :os => "Windows NT", :version => "5.1", :local => "N/A" } 74 | }, 75 | { 76 | :browser => { :name => "MSIE", :version => "6.0" }, 77 | :machine => { :platform => "compatible", :os => "Windows NT", :version => "5.1", :local => "N/A" } 78 | }, 79 | { 80 | :browser => { :name => "MSIE", :version => "5.00" }, 81 | :machine => { :platform => "compatible", :os => "Windows", :version => "98", :local => "N/A" } 82 | }, 83 | { 84 | :browser => { :name => "Opera", :version => "9.80" }, 85 | :machine => { :platform => "Windows NT 6.1", :os => "N/A", :version => "N/A", :local => "es-ES" } 86 | } 87 | ] 88 | count = 0 89 | agents.each do |agent| 90 | info = Rackamole::Utils::AgentDetect.parse( agent ) 91 | expected = expectations[count] 92 | info.should_not be_nil 93 | %w(name version).each { |t| info[:browser][t.to_sym].should == expected[:browser][t.to_sym] } 94 | %w(platform os version).each { |t| info[:machine][t.to_sym].should == expected[:machine][t.to_sym] } 95 | count += 1 96 | end 97 | end 98 | end 99 | 100 | describe "failure" do 101 | it "should not crap out if the user agent is not parsable" do 102 | info = Rackamole::Utils::AgentDetect.parse( "Firefox" ) 103 | %w(browser machine).each { |k| info[k.to_sym].each_pair { |i,v| v.should == (i == :name ? "Firefox" : "N/A") } } 104 | end 105 | 106 | it "should produce an empty info object if nothing can be detected" do 107 | agents = [ 108 | "Oper/8.65 (X11 Linux i686 U ru)", 109 | ] 110 | agents.each do |agent| 111 | info = Rackamole::Utils::AgentDetect.parse( agent ) 112 | info.should_not be_nil 113 | %w(browser machine).each { |k| info[k.to_sym].each_pair { |i,v| v.should == "N/A" } } 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /spec/rackamole/store/mongo_db_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. spec_helper])) 2 | require 'chronic' 3 | 4 | describe Rackamole::Store::MongoDb do 5 | 6 | describe "#mole" do 7 | 8 | before( :all ) do 9 | @now = Chronic.parse( "11/27/2009" ) 10 | @store = Rackamole::Store::MongoDb.new( 11 | :host => 'localhost', 12 | :port => 27017, 13 | :db_name => 'mole_app_test_mdb', 14 | :logger => Rackamole::Logger.new( :file_name => $stdout, :log_level => 'info' ) ) 15 | @db = @store.database 16 | end 17 | 18 | before( :each ) do 19 | @store.send( :reset! ) 20 | 21 | @args = BSON::OrderedHash.new 22 | @args[:type] = Rackamole.feature 23 | @args[:app_name] = "app" 24 | @args[:environment] = :test 25 | @args[:perf_issue] = false 26 | @args[:ip] = "1.1.1.1" 27 | @args[:browser] = BSON::OrderedHash.new 28 | @args[:browser][:name] = "Ibrowse" 29 | @args[:browser][:version] = "1.X" 30 | @args[:machine] = BSON::OrderedHash.new 31 | @args[:machine][:platform] = "Blee" 32 | @args[:machine][:os] = "Windoze" 33 | @args[:machine][:version] = "10.0" 34 | @args[:user_id] = 100 35 | @args[:user_name] = "Fernand" 36 | @args[:request_time] = 1.0 37 | @args[:url] = "http://test_me/" 38 | @args[:path] = "/fred" 39 | @args[:method] = 'GET' 40 | @args[:params] = { :blee => "duh".to_json } 41 | @args[:session] = { :fred => 10.to_json } 42 | @args[:created_at] = @now.utc 43 | end 44 | 45 | it "should mole a context based feature correctly" do 46 | @store.mole( @args ) 47 | @store.features.count.should == 1 48 | @store.logs.count.should == 1 49 | 50 | feature = @store.features.find_one() 51 | feature.should_not be_nil 52 | feature['app'].should == 'app' 53 | feature['env'].should == 'test' 54 | feature['ctx'].should == '/fred' 55 | 56 | log = @store.logs.find_one() 57 | log.should_not be_nil 58 | log['typ'].should == Rackamole.feature 59 | log['fid'].should_not be_nil 60 | log['par'].should == { 'blee' => 'duh'.to_json } 61 | log['ip'].should == '1.1.1.1' 62 | log['bro']['name'].should == "Ibrowse" 63 | log['bro']['version'].should == "1.X" 64 | log['mac']['platform'].should == "Blee" 65 | log['mac']['os'].should == "Windoze" 66 | log['mac']['version'].should == "10.0" 67 | log['url'].should == 'http://test_me/' 68 | log['met'].should == 'GET' 69 | log['ses'].should == { 'fred' => '10' } 70 | log['uid'].should_not be_nil 71 | log['rti'].should == 1.0 72 | log['did'].should == '20091127' 73 | log['tid'].should == '190000' 74 | 75 | feature = @store.features.find_one( log['fid'] ) 76 | feature.should_not be_nil 77 | feature['app'].should == 'app' 78 | feature['env'].should == 'test' 79 | feature['ctx'].should == '/fred' 80 | feature['did'].should == '20091127' 81 | 82 | user = @store.users.find_one( log['uid'] ) 83 | user.should_not be_nil 84 | user['una'].should == "Fernand" 85 | user['uid'].should == 100 86 | user['did'].should == '20091127' 87 | end 88 | 89 | it "should convert a.b.c session keys correctly" do 90 | @args[:session] = { 'a.b.c' => 10 } 91 | 92 | @store.mole( @args ) 93 | @store.features.count.should == 1 94 | @store.logs.count.should == 1 95 | 96 | log = @store.logs.find_one() 97 | log.should_not be_nil 98 | log['ses'].should == { 'a_b_c' => 10 } 99 | end 100 | 101 | it "should mole a rails feature correctly" do 102 | @args[:path] = '/fred/blee/duh' 103 | @args[:route_info] = { :controller => 'fred', :action => 'blee', :id => 'duh' } 104 | @store.mole( @args ) 105 | 106 | @store.features.count.should == 1 107 | @store.logs.count.should == 1 108 | 109 | feature = @store.features.find_one() 110 | feature.should_not be_nil 111 | feature['ctl'].should == 'fred' 112 | feature['act'].should == 'blee' 113 | feature['ctx'].should be_nil 114 | 115 | log = @store.logs.find_one() 116 | log.should_not be_nil 117 | log['typ'].should == Rackamole.feature 118 | log['pat'].should_not be_nil 119 | end 120 | 121 | it "should reuse an existing feature" do 122 | @store.mole( @args ) 123 | @store.mole( @args ) 124 | 125 | @store.features.count.should == 1 126 | @store.logs.count.should == 2 127 | end 128 | 129 | it "should mole perf correctly" do 130 | @args[:type] = Rackamole.perf 131 | @store.mole( @args ) 132 | 133 | @store.features.count.should == 1 134 | @store.logs.count.should == 1 135 | 136 | feature = @store.features.find_one() 137 | feature.should_not be_nil 138 | 139 | log = @store.logs.find_one() 140 | log.should_not be_nil 141 | log['typ'].should == Rackamole.perf 142 | end 143 | 144 | it 'should mole an exception correctly' do 145 | @args[:type] = Rackamole.fault 146 | @args[:stack] = ['fred'] 147 | @args[:fault] = "Oh Snap!" 148 | @store.mole( @args ) 149 | 150 | @store.features.count.should == 1 151 | @store.logs.count.should == 1 152 | 153 | feature = @store.features.find_one() 154 | feature.should_not be_nil 155 | 156 | log = @store.logs.find_one() 157 | log.should_not be_nil 158 | log['typ'].should == Rackamole.fault 159 | log['sta'].should == ['fred'] 160 | log['msg'].should == 'Oh Snap!' 161 | end 162 | 163 | it 'should keep count an similar exceptions or perf issues' do 164 | pending "NYI" 165 | end 166 | end 167 | 168 | describe( "authentication" ) do 169 | it "should authenticate correctly if a store is setup with auth" do 170 | store = Rackamole::Store::MongoDb.new( 171 | :host => 'localhost', 172 | :port => 27017, 173 | :db_name => 'sec_app_test_mdb', 174 | :username => 'fred', 175 | :password => 'letmein', 176 | :logger => Rackamole::Logger.new( :file_name => $stdout, :log_level => 'info' ) ) 177 | end 178 | 179 | it "should crap out if auth failed" do 180 | lambda { 181 | store = Rackamole::Store::MongoDb.new( 182 | :host => 'localhost', 183 | :port => 27017, 184 | :db_name => 'sec_app_test_mdb', 185 | :username => 'fred', 186 | :password => 'hoy', 187 | :logger => Rackamole::Logger.new( :file_name => $stdout, :log_level => 'info' ) ) 188 | }.should raise_error( /Failed to authenticate user 'fred'/ ) 189 | end 190 | end 191 | end -------------------------------------------------------------------------------- /spec/rackamole/alert/emole_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. spec_helper])) 2 | 3 | describe Rackamole::Alert::Emole do 4 | 5 | before( :each ) do 6 | @from = "fernand" 7 | @to = %w[fernand bobo blee] 8 | 9 | Mail.defaults do 10 | delivery_method :test 11 | end 12 | 13 | @options = { :email => { :from => @from, :to => @to }, :perf_threshold => 10 } 14 | 15 | @args = BSON::OrderedHash.new 16 | @args[:type] = Rackamole.feature 17 | @args[:method] = "POST" 18 | @args[:status] = 200 19 | @args[:app_name] = 'Test' 20 | @args[:host] = 'Fred' 21 | @args[:user_name] = 'Fernand' 22 | @args[:url] = 'http://bumblebeetuna/fred/blee' 23 | @args[:path] = '/fred/blee' 24 | @args[:software] = "nginx/0.7.64" 25 | @args[:request_time] = 0.55 26 | @args[:ruby_version] = "ruby 1.8.7 (2009-06-12 patchlevel 174) [i686-darwin10.0.0]" 27 | 28 | @args[:params] = BSON::OrderedHash.new 29 | @args[:params][:id] = 10 30 | @args[:params][:fred] = [10,20,30] 31 | 32 | @args[:session] = BSON::OrderedHash.new 33 | @args[:session][:blee] = "Hello World" 34 | @args[:session][:fred] = [10,20,30] 35 | 36 | @args[:headers] = BSON::OrderedHash.new 37 | @args[:headers]['Cache-Control'] = "no-cache" 38 | @args[:headers]['Content-Type'] = "text/html; charset=utf-8" 39 | @args[:headers]['Content-Length'] = "17911" 40 | @args[:headers]['Set-Cookie'] = "fred" 41 | 42 | @args[:browser] = BSON::OrderedHash.new 43 | @args[:browser][:name] = "Chromey" 44 | @args[:browser][:version] = "1.12.23.54" 45 | 46 | @args[:machine] = BSON::OrderedHash.new 47 | @args[:machine][:platform] = "Windoze" 48 | @args[:machine][:os] = "Windows NT" 49 | @args[:machine][:version] = "3.5" 50 | @args[:machine][:local] = "en-us" 51 | 52 | end 53 | 54 | describe "#alert" do 55 | 56 | it "should send a feature email correctly" do 57 | alert = Rackamole::Alert::Emole.deliver_alert( nil, @options, @args ) 58 | alert.body.to_s.should == feature_body 59 | alert.subject.should == "Rackamole on Test.Fred for user Fernand" 60 | alert.from.should == ["fernand"] 61 | alert.to.should == ["fernand", 'bobo', 'blee'] 62 | end 63 | 64 | it "should send a perf email correctly" do 65 | @args[:type] = Rackamole.perf 66 | @args[:request_time] = 15.2 67 | 68 | alert = Rackamole::Alert::Emole.deliver_alert( nil, @options, @args ) 69 | alert.body.to_s.should == perf_body 70 | alert.subject.should == "Rackamole 15.20 on Test.Fred for user Fernand" 71 | alert.from.should == ["fernand"] 72 | alert.to.should == ["fernand", 'bobo', 'blee'] 73 | end 74 | 75 | it "should send a fault email correctly" do 76 | @args[:type] = Rackamole.fault 77 | @args[:fault] = 'Oh Snap!' 78 | @args[:stack] = ['fred', 'blee'] 79 | alert = Rackamole::Alert::Emole.deliver_alert( nil, @options, @args ) 80 | alert.body.to_s.should == fault_body 81 | alert.subject.should == "Rackamole on Test.Fred for user Fernand" 82 | alert.from.should == ["fernand"] 83 | alert.to.should == ["fernand", 'bobo', 'blee'] 84 | end 85 | end 86 | 87 | def feature_body 88 | msg=< Rackamole::Store::MongoDb.new( 20 | # :db_name => 'mole_blee_development_mdb', 21 | # :username => 'fernand', 22 | # :password => 'letmein', 23 | # ) 24 | # } 25 | # 26 | # === NOTE 27 | # 28 | # To use in conjunction with Wackamole your db_name must follow 29 | # the convention "mole_[app_name]_[environment]_mdb". 30 | # 31 | # === Options 32 | # 33 | # :host :: The name of the host running the mongo server. Default: localhost 34 | # :port :: The port for the mongo server instance. Default: 27017 35 | # :db_name :: The name of the mole databaase. Default: mole_mdb 36 | # :username :: username if the mongo db has auth setup. optional 37 | # :password :: password if the mongo db has auth required. optional 38 | # 39 | def initialize( options={} ) 40 | opts = default_options.merge( options ) 41 | validate_options( opts ) 42 | init_mongo( opts ) 43 | end 44 | 45 | def to_yaml( opts={} ) 46 | YAML::quick_emit( object_id, opts ) do |out| 47 | out.map( taguri, to_yaml_style ) do |map| 48 | map.add( :host , host ) 49 | map.add( :port , port ) 50 | map.add( :db_name , db_name ) 51 | end 52 | end 53 | end 54 | 55 | # Dump mole info to a mongo database. There are actually 2 collections 56 | # for mole information. Namely features and logs. The features collection hold 57 | # application and feature information and is referenced in the mole log. The logs 58 | # collections holds all information that was gathered during the request 59 | # such as user, params, session, request time, etc... 60 | def mole( arguments ) 61 | return if arguments.empty? 62 | 63 | unless @connection 64 | init_mongo( :host => host, :port => port, :db_name => db_name ) 65 | end 66 | 67 | # get a dup of the args since will mock with the original 68 | args = arguments.clone 69 | 70 | # dump request info to mongo 71 | save_log( save_user( args ), save_feature( args ), args ) 72 | rescue => mole_boom 73 | $stderr.puts "MOLE STORE CRAPPED OUT -- #{mole_boom}" 74 | $stderr.puts mole_boom.backtrace.join( "\n " ) 75 | end 76 | 77 | # ======================================================================= 78 | private 79 | 80 | # Clear out mole database content ( Careful there - testing only! ) 81 | def reset! 82 | logs.remove 83 | features.remove 84 | users.remove 85 | end 86 | 87 | def init_mongo( opts ) 88 | @host = opts[:host] 89 | @port = opts[:port] 90 | @db_name = opts[:db_name] 91 | 92 | @connection = Mongo::Connection.new( @host, @port, :logger => opts[:logger] ) 93 | @database = @connection.db( @db_name ) 94 | 95 | if opts[:username] and opts[:password] 96 | authenticated = @database.authenticate( opts[:username], opts[:password] ) 97 | raise "Authentication failed for database #{@db_name}. Please check your credentials and try again" unless authenticated 98 | end 99 | 100 | @features = database.collection( 'features' ) 101 | @logs = database.collection( 'logs' ) 102 | @users = database.collection( 'users' ) 103 | end 104 | 105 | # Validates option hash. 106 | def validate_options( opts ) 107 | %w[host port db_name].each do |option| 108 | unless opts[option.to_sym] 109 | raise "[MOle] Mongo store configuration error -- You must specify a value for option `:#{option}" 110 | end 111 | end 112 | # check for auth 113 | if opts[:username] 114 | %w(username password).each do |option| 115 | unless opts[option.to_sym] 116 | raise "[MOle] Mongo store configuration error -- You must specify a value for auth option `:#{option}" 117 | end 118 | end 119 | end 120 | end 121 | 122 | # Set up mongo default options ie localhost host, default mongo port and 123 | # the database being mole_mdb 124 | def default_options 125 | { 126 | :host => 'localhost', 127 | :port => Mongo::Connection::DEFAULT_PORT 128 | } 129 | end 130 | 131 | # Find or create a moled user... 132 | # BOZO !! What to do if user name changed ? 133 | def save_user( args ) 134 | user_id = args.delete( :user_id ) if args.has_key?( :user_id ) 135 | user_name = args.delete( :user_name ) if args.has_key?( :user_name ) 136 | 137 | row = {} 138 | if user_id and user_name 139 | row = { min_field( :user_id ) => user_id, min_field( :user_name ) => user_name } 140 | else 141 | row = { min_field( :user_name ) => user_name } 142 | end 143 | 144 | user = users.find_one( row, :fields => ['_id'] ) 145 | return user['_id'] if user 146 | 147 | now = args[:created_at] 148 | row[min_field(:date_id)] = "%4d%02d%02d" % [now.year, now.month, now.day] 149 | 150 | users.save( row ) 151 | end 152 | 153 | # Find or create a mole feature... 154 | def save_feature( args ) 155 | app_name = args.delete( :app_name ) 156 | route_info = args.delete( :route_info ) 157 | environment = args.delete( :environment ) 158 | 159 | row = { min_field(:app_name) => app_name, min_field(:env) => environment.to_s } 160 | if route_info 161 | row[min_field(:controller)] = route_info[:controller] 162 | row[min_field(:action)] = route_info[:action] 163 | else 164 | row[min_field(:context)] = args.delete( :path ) 165 | end 166 | 167 | feature = features.find_one( row, :fields => ['_id'] ) 168 | return feature['_id'] if feature 169 | 170 | now = args[:created_at] 171 | row[min_field(:date_id)] = "%4d%02d%02d" %[now.year, now.month, now.day] 172 | 173 | features.save( row ) 174 | end 175 | 176 | # Insert a new feature in the db 177 | # NOTE : Using min key to reduce storage needs. I know not that great for higher level api's :-( 178 | # also saving date and time as ints. same deal... 179 | def save_log( user_id, feature_id, args ) 180 | now = args.delete( :created_at ) 181 | row = { 182 | min_field( :type ) => args[:type], 183 | min_field( :feature_id ) => feature_id, 184 | min_field( :user_id ) => user_id, 185 | min_field( :date_id ) => "%4d%02d%02d" %[now.year, now.month, now.day], 186 | min_field( :time_id ) => "%02d%02d%02d" %[now.hour, now.min, now.sec] 187 | } 188 | 189 | args.each do |k,v| 190 | row[min_field(k)] = check_hash( v ) if v 191 | end 192 | logs.save( row ) 193 | end 194 | 195 | # Check for invalid key format - ie something that will choke mongo 196 | # case a.b.c => a_b_c 197 | def ensure_valid_key( key ) 198 | key.to_s.index( /\./ ) ? key.to_s.gsub( /\./, '_' ) : key 199 | end 200 | 201 | # Check 202 | def check_hash( value ) 203 | return value unless value.is_a?( Hash ) 204 | value.keys.inject({}){ |h,k| h[ensure_valid_key(k)] = value[k];h } 205 | end 206 | 207 | # For storage reason minify the json to save space... 208 | def min_field( field ) 209 | Rackamole::Store::MongoDb.field_map[field] || field 210 | end 211 | 212 | # Normalize all accessors to 3 chars. 213 | def self.field_map 214 | @field_map ||= { 215 | :env => :env, 216 | :app_name => :app, 217 | :context => :ctx, 218 | :controller => :ctl, 219 | :action => :act, 220 | :type => :typ, 221 | :feature_id => :fid, 222 | :date_id => :did, 223 | :time_id => :tid, 224 | :user_id => :uid, 225 | :user_name => :una, 226 | :browser => :bro, 227 | :machine => :mac, 228 | :host => :hos, 229 | :software => :sof, 230 | :request_time => :rti, 231 | :performance => :per, 232 | :method => :met, 233 | :path => :pat, 234 | :session => :ses, 235 | :params => :par, 236 | :ruby_version => :ver, 237 | :fault => :msg, 238 | :stack => :sta, 239 | :created_at => :cro, 240 | :status => :sts, 241 | :headers => :hdr, 242 | :body => :bod 243 | } 244 | end 245 | end 246 | end 247 | end -------------------------------------------------------------------------------- /lib/rackamole/mole.rb: -------------------------------------------------------------------------------- 1 | require 'hitimes' 2 | require 'json' 3 | require 'mongo' 4 | require 'yaml' 5 | 6 | # TODO - add recording for response 7 | # TODO - need plugable archictecture for alerts and stores 8 | module Rack 9 | class Mole 10 | 11 | # Initialize The Mole rack component. It is recommended that you specify at a minimum a user_key to track 12 | # interactions on a per user basis. If you wish to use the mole for the same application in different 13 | # environments you should set the environment attribute RAILS_ENV for example. The perf_threshold setting 14 | # is also recommended to get performance notifications should your web app start sucking. 15 | # By default your app will be moleable upon installation and you should see mole features spewing out in your 16 | # logs. This is the default setting. Alternatively you can store mole information in a mongo database by 17 | # specifying a different store option. 18 | # 19 | # === Options 20 | # 21 | # :config_file :: This option will load rackamole options from a file versus individual options. 22 | # You can leverage yaml and erb with the current rackamole context to specify each of 23 | # the following options but within a yaml file. This gives more flexibility to customize 24 | # the rack component for specific environment. You can specify the :environment option or 25 | # the default will be development. 26 | # :app_name :: The name of the application (Default: Moled App) 27 | # :log_level :: Rackamole logger level. (Default: info ) 28 | # :environment :: The environment for the application ie :environment => RAILS_ENV 29 | # :perf_threshold :: Any request taking longer than this value will get moled. Default: 10secs 30 | # :moleable :: Enable/Disable the MOle (Default:true) 31 | # :store :: The storage instance ie log file or mongodb [Default:stdout] 32 | # :expiration :: Number of seconds to alert expiration. The mole will not keep sending alert if a particular 33 | # mole type has been reported in the past. This threshold specifies the limit at which 34 | # the previously sent alerts will expire and thus will be sent again. 35 | # For instance, it might be the case that the app is consistently slow for a particular action. 36 | # On the first encounter an alert will be sent ( if configured ). Any subsequent requests for this action 37 | # will not fire an alert until the expiration threshold is hit. The default is 1 hour. 38 | # Setting this threshold to Rackamole::Stash::Collector::NEVER will result in alerts being fired continually. 39 | # :user_key :: If sessions are enable, this represents the session key for the user name or 40 | # user_id. 41 | # == 42 | # If the username resides in the session hash with key :user_name the you can use: 43 | # :user_key => :user_name 44 | # Or you can eval it on the fly - though this will be much slower and not recommended 45 | # :user_key => { :session_key => :user_id, :extractor => lambda{ |id| User.find( id ).name} } 46 | # == 47 | # 48 | # :mole_excludes :: Exclude some of the parameters that the mole would normally include. The list of 49 | # customizable parameters is: software, ip, browser type, url, path, status, headers and body. 50 | # This options takes an array of symbols. Defaults to [:body]. 51 | # :perf_excludes :: Specifies a set of paths that will be excluded when a performance issue is detected. 52 | # This is useful when certain requests are known to be slow. When a match is found an 53 | # alert won't be issued but the context will still be moled. 54 | # == 55 | # Don't send an alert when perf is found on /blee/fred. Don't alert on /duh unless the request took longer than 10 secs 56 | # :perf_excludes => [ {:context => '/blee/fred}, {:context => /^\/duh.*/, :threshold => 10 } ] 57 | # == 58 | # :excluded_paths:: Exclude paths that you do not wish to mole by specifying an array of regular expresssions. 59 | # :param_excludes:: Exempt certain sensitive request parameters from being logged to the mole. Specify an array of keys 60 | # as symbols ie [:password, :card_number]. 61 | # :session_excludes:: Exempt session params from being logged by the mole. Specify an array of keys as symbols 62 | # ie [:fred, :blee] to exclude session[:fred] and session[:blee] from being stored. 63 | # :twitter :: Set this option to have the mole twitt certain alerts. You must configure your twitter auth 64 | # via the :username and :password keys and :alert_on with an array of mole types you 65 | # wish to be notified on. 66 | # == 67 | # :twitter => { :username => 'fred', :password => 'blee', :alert_on => [Rackamole.perf, Rackamole.fault] } 68 | # == 69 | # ==== BOZO! currently there is not support for throttling or monitoring these alerts. 70 | # == 71 | # :email :: The mole can be configured to send out emails bases on interesting mole features. 72 | # This feature uses Pony to send out the email. You must specify a hash with the following keys :from, :to 73 | # and :alert_on options to indicate which mole type your wish to be alerted on. 74 | # See Pony docs for custom options for your emails 75 | # == 76 | # :email => { :from => 'fred@acme.com', :to => ['blee@acme.com', 'doh@acme.com'], :alert_on => [Rackamole.perf, Rackamole.fault] } 77 | # == 78 | # 79 | # :growl :: You can set up rackamole to send growl notifications when certain features 80 | # are encounters in the same way email and twitt alerts works. The :to options 81 | # describes a collection of hash values with the ip and optional password to the recipients. 82 | # == 83 | # :growl => { :to => [{ :ip => '1.1.1.1' }], :alert_on => [Rackamole.perf, Rackamole.fault] } 84 | # == 85 | def initialize( app, opts={} ) 86 | @app = app 87 | init_options( opts ) 88 | validate_options 89 | @logger = Rackamole::Logger.new( :logger_name => 'RACKAMOLE', :log_level => options[:log_level] ) 90 | end 91 | 92 | # Entering the MOle zone... 93 | # Watches incoming requests and report usage information. The mole will also track request that 94 | # are taking longer than expected and also report any requests that are raising exceptions. 95 | def call( env ) 96 | # Bail if application is not moleable 97 | return @app.call( env ) unless moleable? 98 | 99 | @stash = env['mole.stash'] if env['mole.stash'] 100 | @stash = Rackamole::Stash::Collector.new( options[:app_name], options[:environment], options[:expiration] ) unless stash 101 | 102 | status, headers, body = nil 103 | elapsed = Hitimes::Interval.measure do 104 | begin 105 | status, headers, body = @app.call( env ) 106 | rescue => boom 107 | env['mole.exception'] = boom 108 | mole_feature( env, elapsed, status, headers, body ) 109 | raise boom 110 | end 111 | end 112 | mole_feature( env, elapsed, status, headers, body ) 113 | return status, headers, body 114 | end 115 | 116 | # =========================================================================== 117 | private 118 | 119 | attr_reader :options, :logger, :stash #:nodoc: 120 | 121 | # Load up configuration options 122 | def init_options( opts ) 123 | if opts[:config_file] && (env = opts[:environment] || "development") 124 | raise "Unable to find rackamole config file #{opts[:config_file]}" unless ::File.exists?( opts[:config_file] ) 125 | begin 126 | opts = YAML.load( ERB.new( IO.read( opts[:config_file] ) ).result( binding ) )[env] 127 | opts[:environment] = env 128 | rescue => boom 129 | raise "Unable to parse Rackamole config file #{boom}" 130 | end 131 | end 132 | @options = default_options.merge( opts ) 133 | end 134 | 135 | # Mole default options 136 | def default_options 137 | { 138 | :moleable => true, 139 | :log_level => :info, 140 | :expiration => 60*60, # 1 hour 141 | :environment => 'development', 142 | :excluded_paths => [/.*?\/stylesheets\/.*/, /.*?\/javascripts\/.*/,/\.*?\/images\/.*/ ], 143 | :mole_excludes => [:body], 144 | :perf_threshold => 10.0, 145 | :store => Rackamole::Store::Log.new 146 | } 147 | end 148 | 149 | # Validates all configured options... Throws error if invalid configuration 150 | def validate_options 151 | return unless options[:moleable] 152 | 153 | %w[app_name environment perf_threshold store].each do |k| 154 | raise "[M()le] -- Unable to locate required option key `#{k}" unless options[k.to_sym] 155 | end 156 | 157 | # Barf early if something is wrong in the configuration 158 | configured?( :twitter, [:username, :password, :alert_on], true ) 159 | configured?( :email , [:from, :to, :alert_on], true ) 160 | configured?( :growl , [:to, :alert_on], true ) 161 | end 162 | 163 | # Send moled info to store and potentially send out alerts... 164 | def mole_feature( env, elapsed, status, headers, body ) 165 | env['mole.stash'] = stash 166 | 167 | attrs = mole_info( env, elapsed, status, headers, body ) 168 | 169 | # If nothing to mole bail out! 170 | return if attrs.empty? 171 | 172 | # send info to configured store 173 | options[:store].mole( attrs ) 174 | 175 | # Check for dups. If we've reported this req before don't report it again... 176 | unless duplicated?( env, attrs ) 177 | # send email alert ? 178 | if alertable?( :email, attrs[:type] ) 179 | logger.debug ">>> Sending out email on mole type #{attrs[:type]} from #{options[:email][:from]} to #{options[:email][:to].join( ", ")}" 180 | Rackamole::Alert::Emole.deliver_alert( logger, options, attrs ) 181 | end 182 | 183 | # send growl alert ? 184 | if alertable?( :growl, attrs[:type] ) 185 | logger.debug ">>> Sending out growl on mole type #{attrs[:type]} to @#{options[:growl][:to].inspect}" 186 | Rackamole::Alert::Growl.deliver_alert( logger, options, attrs ) 187 | end 188 | 189 | # send twitter alert ? 190 | if alertable?( :twitter, attrs[:type] ) 191 | logger.debug ">>> Sending out twitt on mole type #{attrs[:type]} on @#{options[:twitter][:username]}" 192 | Rackamole::Alert::Twitt.deliver_alert( logger, options, attrs ) 193 | end 194 | end 195 | rescue => boom 196 | logger.error "!! MOLE RECORDING CRAPPED OUT !! -- #{boom}" 197 | boom.backtrace.each { |l| logger.error l } 198 | end 199 | 200 | # Check if we've already seen such an error 201 | def duplicated?( env, attrs ) 202 | # Skip features for now... 203 | return true if attrs[:type] == Rackamole.feature 204 | 205 | # Don't bother if expiration is set to never. ie fire alerts all the time 206 | return false if options[:expiration] == Rackamole::Stash::Collector::NEVER 207 | 208 | now = Time.now 209 | app_id = [attrs[:app_name], attrs[:environment]].join( '_' ) 210 | path = attrs[:route_info] ? "#{attrs[:route_info][:controller]}#{attrs[:route_info][:action]}" : attrs[:path] 211 | 212 | # Check expired entries 213 | stash.expire! 214 | 215 | # check if we've seen this error before. If so stash it. 216 | if attrs[:type] == Rackamole.fault 217 | return stash.stash_fault( path, attrs[:stack].first, now.utc ) 218 | end 219 | 220 | # Check if we've seen this perf issue before. If so stash it 221 | if attrs[:type] == Rackamole.perf 222 | # Don't send alert if exempted 223 | return true if perf_exempt?( attrs[:path], attrs[:request_time]) 224 | return stash.stash_perf( path, attrs[:request_time], now.utc ) 225 | end 226 | end 227 | 228 | # Check if request should be exempted from perf alert 229 | def perf_exempt?( path, req_time ) 230 | return false unless option_defined?( :perf_excludes ) 231 | options[:perf_excludes].each do |hash| 232 | context = hash[:context] 233 | threshold = hash[:threshold] 234 | time_trip = (threshold ? req_time <= threshold : true) 235 | 236 | if context.is_a? String 237 | return true if path == context and time_trip 238 | elsif context.is_a? Regexp 239 | return true if path.match(context) and time_trip 240 | end 241 | end 242 | false 243 | end 244 | 245 | # Check if an option is defined 246 | def option_defined?( key ) 247 | options.has_key?(key) and options[key] 248 | end 249 | 250 | # Check if an options is set and configured 251 | def configured?( key, configs, optional=true ) 252 | return false if optional and !options.has_key?(key) 253 | raise "Missing option key :#{key}" unless options.has_key?(key) 254 | configs.each do |c| 255 | raise "Invalid value for option :#{key}. Expecting a hash with symbols [#{configs.join(',')}]" unless options[key].respond_to? :key? 256 | unless options[key].key?(c) 257 | raise "Option :#{key} is not properly configured. Missing #{c.inspect} in [#{options[key].keys.sort{|a,b| a.to_s <=> b.to_s}.join(',')}]" 258 | end 259 | end 260 | true 261 | end 262 | 263 | # Check if feature should be send to alert clients ie email or twitter 264 | def alertable?( filter, type ) 265 | return false unless configured?( filter, [:alert_on] ) 266 | return false unless options[filter][:alert_on] 267 | options[filter][:alert_on].include?( type ) 268 | end 269 | 270 | # Check if this request should be moled according to the exclude filters 271 | def mole_request?( request ) 272 | if options.has_key?( :excluded_paths ) and options[:excluded_paths] 273 | options[:excluded_paths].each do |exclude_path| 274 | exclude_path = Regexp.new( exclude_path ) if exclude_path.is_a?( String ) 275 | if request.path =~ exclude_path 276 | logger.debug ">>> Excluded request -- #{request.path} using exclude flag #{exclude_path}" 277 | return false 278 | end 279 | end 280 | end 281 | true 282 | end 283 | 284 | # Extract interesting information from the request 285 | def mole_info( env, elapsed, status, headers, body ) 286 | request = Rack::Request.new( env ) 287 | response = nil 288 | begin 289 | response = Rack::Response.new( body, status, headers ) 290 | rescue 291 | ; 292 | end 293 | 294 | info = BSON::OrderedHash.new 295 | 296 | return info unless mole_request?( request ) 297 | 298 | session = env['rack.session'] 299 | route = get_route( request ) 300 | 301 | ip, user_agent = identify( env ) 302 | user_id = nil 303 | user_name = nil 304 | 305 | # BOZO !! This could be slow if have to query db to get user name... 306 | # Preferred store username in session and give at key 307 | user_key = options[:user_key] 308 | if session and user_key 309 | if user_key.instance_of? Hash 310 | user_id = session[ user_key[:session_key] ] 311 | if user_key[:extractor] 312 | user_name = user_key[:extractor].call( user_id ) 313 | end 314 | else 315 | user_name = session[user_key] 316 | end 317 | end 318 | 319 | info[:type] = (elapsed and elapsed > options[:perf_threshold] ? Rackamole.perf : Rackamole.feature) 320 | info[:app_name] = options[:app_name] 321 | info[:environment] = options[:environment] || "Unknown" 322 | info[:user_id] = user_id if user_id 323 | info[:user_name] = user_name || "Unknown" 324 | info[:host] = env['SERVER_NAME'] 325 | info[:request_time] = elapsed if elapsed 326 | info[:method] = env['REQUEST_METHOD'] 327 | info[:route_info] = route if route 328 | info[:created_at] = Time.now.utc 329 | 330 | # Optional elements... 331 | mole?( :software , info, env['SERVER_SOFTWARE'] ) 332 | mole?( :ip , info, ip ) 333 | mole?( :url , info, request.url ) 334 | mole?( :path , info, request.path ) 335 | mole?( :status , info, status ) 336 | mole?( :headers , info, headers ) 337 | mole?( :body , info, response.body ) if response 338 | 339 | # Gather up browser and client machine info 340 | agent_info = Rackamole::Utils::AgentDetect.parse( user_agent ) 341 | %w(browser machine).each { |type| mole?(type.to_sym, info, agent_info[type.to_sym] ) } 342 | 343 | # Dump request params 344 | unless request.params.empty? 345 | info[:params] = filter_params( request.params, options[:param_excludes] || [] ) 346 | end 347 | if route 348 | info[:params] ||= {} 349 | info[:params].merge!( filter_params( params_from_route( route ), options[:param_excludes] || [] ) ) 350 | end 351 | 352 | # Dump session var 353 | if session and !session.empty? 354 | info[:session] = filter_params( session, options[:session_excludes] || [] ) 355 | end 356 | 357 | # Check if an exception was raised. If so consume it and clear state 358 | exception = env['mole.exception'] 359 | if exception 360 | info[:ruby_version] = %x[ruby -v] 361 | info[:fault] = exception.to_s 362 | info[:stack] = trim_stack( exception ) 363 | info[:type] = Rackamole.fault 364 | env['mole.exception'] = nil 365 | end 366 | info 367 | end 368 | 369 | # Parse out rails request params if in rails env 370 | def params_from_route( route ) 371 | params = {} 372 | except = [:controller, :action] 373 | route.each_pair do |k,v| 374 | next if except.include?( k ) 375 | params[k] = v 376 | end 377 | params 378 | end 379 | 380 | # check exclusion to see if the information should be moled 381 | def mole?( key, stash, value ) 382 | return stash[key] = value if !options[:mole_excludes] or options[:mole_excludes].empty? 383 | stash[key] = (options[:mole_excludes].include?( key ) ? nil : value) 384 | end 385 | 386 | # filters out params hash and convert values to json 387 | def filter_params( source, excludes, nest=false ) 388 | results = nest ? {} : BSON::OrderedHash.new 389 | source.keys.sort{ |a,b| a.to_s <=> b.to_s }.each do |k| 390 | unless excludes.include? k.to_sym 391 | results[k.to_sym] = if source[k].is_a?(Hash) 392 | filter_params( source[k], excludes, true) 393 | else 394 | source[k] 395 | end 396 | results[k.to_sym] = results[k.to_sym].to_json unless nest 397 | end 398 | end 399 | results 400 | end 401 | 402 | # Trim stack trace 403 | def trim_stack( boom ) 404 | boom.backtrace[0...4] 405 | end 406 | 407 | # Identify request ie ip and browser configuration 408 | def identify( request_env ) 409 | return request_env['HTTP_X_FORWARDED_FOR'] || request_env['REMOTE_ADDR'], request_env['HTTP_USER_AGENT'] 410 | end 411 | 412 | # Checks if this application is moleable 413 | def moleable? 414 | options[:moleable] 415 | end 416 | 417 | # Fetch route info if any... 418 | def get_route( request ) 419 | return nil unless defined?( RAILS_ENV ) 420 | # Check for invalid route exception... 421 | begin 422 | return Rails.application.routes.recognize_path( request.path, {:method => request.request_method.downcase.to_sym } ) 423 | rescue => boom 424 | return nil 425 | end 426 | end 427 | 428 | # # Debug - Dump env to stdout 429 | # def dump( env, level=0 ) 430 | # env.keys.sort{ |a,b| a.to_s <=> b.to_s }.each do |k| 431 | # value = env[k] 432 | # if value.respond_to?(:each_pair) 433 | # puts "%s %-#{40-level}s" % [' '*level,k] 434 | # dump( env[k], level+1 ) 435 | # elsif value.instance_of?(::ActionController::Request) or value.instance_of?(::ActionController::Response) 436 | # puts "%s %-#{40-level}s %s" % [ ' '*level, k, value.class ] 437 | # else 438 | # puts "%s %-#{40-level}s %s" % [ ' '*level, k, value.inspect ] 439 | # end 440 | # end 441 | # end 442 | end 443 | end 444 | -------------------------------------------------------------------------------- /spec/rackamole/mole_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. spec_helper])) 2 | 3 | describe Rack::Mole do 4 | include Rack::Test::Methods 5 | 6 | before :each do 7 | @response = [ 200, {"Content-Type" => "text/plain"}, ["success"] ] 8 | @test_store = TestStore.new 9 | @test_env = { 10 | 'rack.session' => { :user_id => 100, :username => "fernand" }, 11 | 'HTTP_X_FORWARDED_FOR' => '1.1.1.1', 12 | 'HTTP_USER_AGENT' => "Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.0.12) Gecko/20080326 CentOS/1.5.0.12-14.el5.centos Firefox/1.5.0.12" 13 | } 14 | @opts = { 15 | :app_name => "Test App", 16 | :environment => :test, 17 | :perf_threshold => 0.1, 18 | :user_key => :username, 19 | :store => @test_store 20 | } 21 | end 22 | 23 | class TestStore 24 | attr_accessor :mole_result 25 | def mole( args ) 26 | @mole_result = args 27 | end 28 | end 29 | 30 | def app( opts={} ) 31 | response = @response 32 | @app ||= Rack::Builder.new do 33 | use Rack::Lint 34 | use Rack::Mole, opts 35 | run lambda { |env| response } 36 | end 37 | end 38 | 39 | def error_app( opts={} ) 40 | @app ||= Rack::Builder.new do 41 | use Rack::Lint 42 | use Rack::Mole, opts 43 | run lambda { |env| raise "Oh Snap!" } 44 | end 45 | end 46 | 47 | def slow_app( opts={} ) 48 | response = @response 49 | @app ||= Rack::Builder.new do 50 | use Rack::Lint 51 | use Rack::Mole, opts 52 | run lambda { |env| sleep(0.2); response } 53 | end 54 | end 55 | 56 | # --------------------------------------------------------------------------- 57 | describe "fault duplicate" do 58 | before( :each ) do 59 | error_app( @opts ) 60 | end 61 | 62 | it "should mole a fault issue correctly" do 63 | begin 64 | get "/", nil, @test_env 65 | rescue 66 | last_request.env['mole.stash'].should_not be_nil 67 | fault = last_request.env['mole.stash'].send( :find_fault, "/", File.join( File.dirname(__FILE__), "mole_spec.rb:43:in `block (2 levels) in error_app'" ) ) 68 | fault.should_not be_nil 69 | fault.count.should == 1 70 | end 71 | end 72 | 73 | it "should trap a recuring fault on given path correctly" do 74 | env = @test_env 75 | 2.times do |i| 76 | begin 77 | get "/", nil, env 78 | rescue 79 | last_request.env['mole.stash'].should_not be_nil 80 | fault = last_request.env['mole.stash'].send( :find_fault, "/", File.join( File.dirname(__FILE__), "mole_spec.rb:43:in `block (2 levels) in error_app'" ) ) 81 | fault.should_not be_nil 82 | fault.count.should == i+1 83 | env = last_request.env 84 | end 85 | end 86 | end 87 | 88 | it "should trap a recuring fault on different path correctly" do 89 | env = @test_env 90 | 2.times do |i| 91 | begin 92 | env['PATH_INFO'] = "/#{i}" 93 | get "/#{i}", nil, env 94 | rescue => boom 95 | last_request.env['mole.stash'].should_not be_nil 96 | fault = last_request.env['mole.stash'].send( :find_fault, "/", File.join( File.dirname(__FILE__), "mole_spec.rb:43:in `block (2 levels) in error_app'" ) ) 97 | fault.should_not be_nil 98 | fault.count.should == i+1 99 | env = last_request.env 100 | end 101 | end 102 | end 103 | end 104 | 105 | # --------------------------------------------------------------------------- 106 | describe "perfomance exemptions" do 107 | it "should exempt a string path correctly" do 108 | rack = Rack::Mole.new( nil, :app_name => "test app", :perf_excludes => [ {:context => "/fred/blee" } ] ) 109 | rack.send( :perf_exempt?, "/fred/blee", 10 ).should == true 110 | rack.send( :perf_exempt?, "/fred/blee1", 10 ).should == false 111 | end 112 | 113 | it "should exempt a regex path correctly" do 114 | rack = Rack::Mole.new( nil, :app_name => "test app", :perf_excludes => [ {:context => /^\/fred\/?.*/ } ] ) 115 | rack.send( :perf_exempt?, "/fred/blee", 10 ).should == true 116 | rack.send( :perf_exempt?, "/fred", 10 ).should == true 117 | rack.send( :perf_exempt?, "/fred/blee/bubba", 10 ).should == true 118 | rack.send( :perf_exempt?, "/freud", 10 ).should == false 119 | end 120 | 121 | it "should exempt path with threshold correctly" do 122 | rack = Rack::Mole.new( nil, :app_name => "test app", :perf_excludes => [ {:context => /^\/fred\/?.*/, :threshold => 15 } ] ) 123 | rack.send( :perf_exempt?, "/fred/blee", 10 ).should == true 124 | rack.send( :perf_exempt?, "/fred/blee", 16 ).should == false 125 | end 126 | 127 | it "should exempt an array of path correctly" do 128 | excludes = [ 129 | { :context => "/duh/1" , :threshold => 5 }, 130 | { :context => /^\/fred\/?.*/, :threshold => 15 }, 131 | ] 132 | rack = Rack::Mole.new( nil, :app_name => "test app", :perf_excludes => excludes ) 133 | rack.send( :perf_exempt?, "/fred/blee", 10 ).should == true 134 | rack.send( :perf_exempt?, "/crap/10/fred", 10 ).should == false 135 | rack.send( :perf_exempt?, "/fred/blee", 16 ).should == false 136 | 137 | rack.send( :perf_exempt?, "/duh/1", 5 ).should == true 138 | rack.send( :perf_exempt?, "/duh/1", 6 ).should == false 139 | rack.send( :perf_exempt?, "/duh/2", 6 ).should == false 140 | end 141 | end 142 | 143 | # --------------------------------------------------------------------------- 144 | describe "performance duplicate" do 145 | before( :each ) do 146 | @test_store = TestStore.new 147 | slow_app( @opts ) 148 | end 149 | 150 | it "should mole a perf issue correctly" do 151 | get "/", nil, @test_env 152 | last_request.env['mole.stash'].should_not be_nil 153 | perf = last_request.env['mole.stash'].send( :find_perf, "/" ) 154 | perf.should_not be_nil 155 | perf.count.should == 1 156 | end 157 | 158 | it "should trap a recuring perf on given path correctly" do 159 | env = @test_env 160 | 2.times do |i| 161 | get "/", nil, env 162 | perf = last_request.env['mole.stash'].send( :find_perf, "/" ) 163 | perf.should_not be_nil 164 | perf.count.should == i+1 165 | env = last_request.env 166 | end 167 | end 168 | 169 | it "should trap a recuring perf on different path correctly" do 170 | env = @test_env 171 | 2.times do |i| 172 | env['PATH_INFO'] = "/#{i}" 173 | get "/#{i}", nil, env 174 | last_request.env['mole.stash'].should_not be_nil 175 | count = 0 176 | while count <= i 177 | perf = last_request.env['mole.stash'].send( :find_perf, "/#{count}" ) 178 | perf.should_not be_nil 179 | perf.count.should == 1 180 | count += 1 181 | end 182 | env = last_request.env 183 | end 184 | end 185 | end 186 | 187 | # --------------------------------------------------------------------------- 188 | it "should mole a framwework exception correctly" do 189 | error_app( @opts ) 190 | begin 191 | get "/", nil, @test_env 192 | rescue 193 | @test_store.mole_result[:stack].should have(4).items 194 | last_request.env['mole.stash'].should_not be_nil 195 | fault = last_request.env['mole.stash'].send( :find_fault, "/", File.join( File.dirname(__FILE__), "mole_spec.rb:43:in `block (2 levels) in error_app'" ) ) 196 | fault.should_not be_nil 197 | fault.count.should == 1 198 | end 199 | end 200 | 201 | # --------------------------------------------------------------------------- 202 | describe "exclusions" do 203 | before( :each) do 204 | opts = @opts.clone 205 | opts[:mole_excludes] = [:headers, :body, :browser, :ip, :url] 206 | app( opts ) 207 | end 208 | 209 | it "should exclude some mole attributes correctly" do 210 | get "/fred/blee", nil, @test_env 211 | 212 | @test_store.mole_result[:app_name].should == "Test App" 213 | @test_store.mole_result[:environment].should == :test 214 | @test_store.mole_result[:user_id].should be_nil 215 | @test_store.mole_result[:user_name].should == 'fernand' 216 | @test_store.mole_result[:method].should == 'GET' 217 | @test_store.mole_result[:path].should == '/fred/blee' 218 | @test_store.mole_result[:type].should == Rackamole.feature 219 | @test_store.mole_result[:params].should be_nil 220 | @test_store.mole_result[:session].should_not be_nil 221 | @test_store.mole_result[:session].keys.should have(2).items 222 | @test_store.mole_result[:status].should == 200 223 | @test_store.mole_result[:machine].should_not be_nil 224 | 225 | # Excluded 226 | @test_store.mole_result[:headers].should be_nil 227 | @test_store.mole_result[:body].should be_nil 228 | @test_store.mole_result[:browser].should be_nil 229 | @test_store.mole_result[:ip].should be_nil 230 | @test_store.mole_result[:url].should be_nil 231 | end 232 | end 233 | 234 | # --------------------------------------------------------------------------- 235 | describe 'moling a request' do 236 | before :each do 237 | app( @opts ) 238 | end 239 | 240 | it "should set the mole meta correctly" do 241 | get "/fred/blee", nil, @test_env 242 | 243 | @test_store.mole_result[:app_name].should == "Test App" 244 | @test_store.mole_result[:environment].should == :test 245 | @test_store.mole_result[:user_id].should be_nil 246 | @test_store.mole_result[:user_name].should == 'fernand' 247 | @test_store.mole_result[:ip].should == '1.1.1.1' 248 | @test_store.mole_result[:browser][:name].should == "Firefox" 249 | @test_store.mole_result[:browser][:version].should == '1.5.0.12' 250 | @test_store.mole_result[:machine][:platform].should == 'X11' 251 | @test_store.mole_result[:machine][:os].should == 'Linux' 252 | @test_store.mole_result[:machine][:version].should == 'i686' 253 | @test_store.mole_result[:method].should == 'GET' 254 | @test_store.mole_result[:url].should == 'http://example.org/fred/blee' 255 | @test_store.mole_result[:path].should == '/fred/blee' 256 | @test_store.mole_result[:type].should == Rackamole.feature 257 | @test_store.mole_result[:params].should be_nil 258 | @test_store.mole_result[:session].should_not be_nil 259 | @test_store.mole_result[:session].keys.should have(2).items 260 | @test_store.mole_result[:status].should == 200 261 | @test_store.mole_result[:headers].should == { "Content-Type" => "text/plain" } 262 | @test_store.mole_result[:body].should be_nil 263 | end 264 | 265 | it "mole an exception correctly" do 266 | begin 267 | raise 'Oh snap!' 268 | rescue => boom 269 | @test_env['mole.exception'] = boom 270 | get "/crap/out", nil, @test_env 271 | @test_store.mole_result[:type].should == Rackamole.fault 272 | @test_store.mole_result[:stack].should have(4).items 273 | @test_store.mole_result[:fault].should == 'Oh snap!' 274 | last_request.env['mole.stash'].should_not be_nil 275 | fault = last_request.env['mole.stash'].send( :find_fault, "/", File.join( File.dirname(__FILE__), "mole_spec.rb:267:in `block (3 levels) in '" ) ) 276 | fault.should_not be_nil 277 | fault.count.should == 1 278 | end 279 | end 280 | 281 | it "should capture request parameters correctly" do 282 | get "/", { :blee => 'duh' }, @test_env 283 | @test_store.mole_result[:params].should == { :blee => "duh".to_json } 284 | end 285 | 286 | it "should not mole a standard exclusion" do 287 | %w(/stylesheets/style.css /javascripts/blee.js /images/fred.png).each do |path| 288 | get path, nil, @test_env 289 | @test_store.mole_result.should be_nil 290 | end 291 | end 292 | 293 | it "should not mole a custom exclusion" do 294 | @opts[:excluded_paths] = [/\/should_bail/] 295 | get '/should_bail', nil, @test_env 296 | @test_store.mole_result.should be_nil 297 | end 298 | end 299 | 300 | # --------------------------------------------------------------------------- 301 | describe 'username in session' do 302 | it "should pickup the user name from the session correctly" do 303 | app( @opts ) 304 | get "/", nil, @test_env 305 | @test_store.mole_result[:user_id].should be_nil 306 | @test_store.mole_result[:user_name].should == 'fernand' 307 | end 308 | 309 | it "should extract a username correctly" do 310 | @opts[:user_key] = { :session_key => :user_id, :extractor => lambda { |k| "Fernand #{k}" } } 311 | app( @opts ) 312 | get "/", nil, @test_env 313 | @test_store.mole_result[:user_id].should == 100 314 | @test_store.mole_result[:user_name].should == 'Fernand 100' 315 | end 316 | end 317 | 318 | describe "rails env" do 319 | it "should find route info correctly" do 320 | pending do 321 | RAILS_ENV = true 322 | ActionController::Routing::Routes.stub!( :recognize_path ).and_return( { :controller => 'fred', :action => 'blee' } ) 323 | rack = Rack::Mole.new( nil, :app_name => "test app" ) 324 | 325 | # routes.should_receive( 'recognize_path' ).with( 'fred', { :method => 'blee' } ).and_return( ) 326 | res = rack.send( :get_route, OpenStruct.new( :path => "/", :request_method => "GET") ) 327 | res.should_not be_nil 328 | res[:controller].should == 'fred' 329 | res[:action].should == 'blee' 330 | end 331 | end 332 | 333 | it "should extract request parameters correctly" do 334 | rack = Rack::Mole.new( nil, :app_name => "test app" ) 335 | res = rack.send( :params_from_route, {:controller => "blee", :action => "fred", :bobo => "hello" } ) 336 | res.should_not be_nil 337 | res.should have(1).item 338 | res.should == { :bobo => "hello" } 339 | end 340 | 341 | it "should not pick up params if none are specified" do 342 | rack = Rack::Mole.new( nil, :app_name => "test app" ) 343 | res = rack.send( :params_from_route, {:controller => "blee", :action => "fred" } ) 344 | res.should be_empty 345 | end 346 | end 347 | 348 | # --------------------------------------------------------------------------- 349 | describe 'sending alerts' do 350 | it "should send out alerts on the first occurrance of a perf issue" do 351 | Rackamole::Alert::Twitt.stub!( :deliver_alert ) 352 | Rackamole::Alert::Emole.stub!( :deliver_alert ) 353 | 354 | @opts[:twitter] = { :username => "fred", :password => "blee", :alert_on => [Rackamole.perf] } 355 | @opts[:email] = { :from => "fred", :to => ["blee"], :alert_on => [Rackamole.perf] } 356 | 357 | slow_app( @opts ) 358 | 359 | Rackamole::Alert::Emole.should_receive( :deliver_alert ).once 360 | Rackamole::Alert::Twitt.should_receive( :deliver_alert ).once 361 | 362 | get "/", nil, @test_env 363 | end 364 | 365 | it "should should not send several alerts on an occurance of the same issue" do 366 | Rackamole::Alert::Twitt.stub!( :deliver_alert ) 367 | Rackamole::Alert::Emole.stub!( :deliver_alert ) 368 | 369 | @opts[:twitter] = { :username => "fred", :password => "blee", :alert_on => [Rackamole.perf] } 370 | @opts[:email] = { :from => "fred", :to => ["blee"], :alert_on => [Rackamole.perf] } 371 | 372 | slow_app( @opts ) 373 | 374 | env = @test_env 375 | # First time ok 376 | Rackamole::Alert::Emole.should_receive( :deliver_alert ).once 377 | Rackamole::Alert::Twitt.should_receive( :deliver_alert ).once 378 | get "/", nil, env 379 | env = last_request.env 380 | # Second time - no alerts 381 | Rackamole::Alert::Emole.should_not_receive( :deliver_alert ) 382 | Rackamole::Alert::Twitt.should_not_receive( :deliver_alert ) 383 | get "/", nil, env 384 | end 385 | 386 | end 387 | 388 | # --------------------------------------------------------------------------- 389 | describe '#alertable?' do 390 | before( :each ) do 391 | @rack = Rack::Mole.new( nil, 392 | :app_name => "test app", 393 | :twitter => { 394 | :username => 'fred', 395 | :password => 'blee', 396 | :alert_on => [Rackamole.perf, Rackamole.fault] 397 | }, 398 | :email => { 399 | :from => 'fred', 400 | :to => ['blee'], 401 | :alert_on => [Rackamole.perf, Rackamole.fault, Rackamole.feature] 402 | } ) 403 | end 404 | 405 | it "should succeeed if a feature can be twitted on" do 406 | @rack.send( :alertable?, :twitter, Rackamole.perf ).should == true 407 | end 408 | 409 | it "should fail if the type is not in range" do 410 | @rack.send( :alertable?, :twitt_on, 10 ).should == false 411 | end 412 | 413 | it "should fail if this is not an included feature" do 414 | @rack.send( :alertable?, :twitter, Rackamole.feature ).should == false 415 | end 416 | 417 | it "should fail if an alert is not configured" do 418 | @rack.send( :alertable?, :mail_on, Rackamole.perf ).should == false 419 | end 420 | end 421 | 422 | # --------------------------------------------------------------------------- 423 | describe '#configured?' do 424 | before( :each ) do 425 | options = { 426 | :app_name => "test app", 427 | :blee => [1,2,3], 428 | :twitter => { :username => 'Fernand', :password => "Blee", :alert_on => [Rackamole.perf, Rackamole.fault] }, 429 | } 430 | @rack = Rack::Mole.new( nil, options ) 431 | end 432 | 433 | it "should return true if an option is correctly configured" do 434 | @rack.send( :configured?, :twitter, [:username, :password] ).should == true 435 | @rack.send( :configured?, :twitter, [:alert_on] ).should == true 436 | end 437 | 438 | it "should fail if an option is not set" do 439 | lambda { 440 | @rack.send( :configured?, :twitter, [:username, :password, :blee] ) 441 | }.should raise_error(RuntimeError, /Option \:twitter is not properly configured. Missing \:blee in \[alert_on,password,username\]/) 442 | end 443 | 444 | it "should fail if an option is not a hash" do 445 | lambda { 446 | @rack.send( :configured?, :blee, [:username, :pwd] ) 447 | }.should raise_error(RuntimeError, /Invalid value for option \:blee\. Expecting a hash with symbols \[username,pwd\]/ ) 448 | end 449 | 450 | it "should fail if an option is not correctly configured" do 451 | lambda { 452 | @rack.send( :configured?, :fred, [:username, :pwd], false ) 453 | }.should raise_error(RuntimeError, /Missing option key \:fred/ ) 454 | end 455 | end 456 | 457 | # --------------------------------------------------------------------------- 458 | # describe '#id_browser' do 459 | # before :all do 460 | # @rack = Rack::Mole.new( nil, :app_name => "test app" ) 461 | # end 462 | # 463 | # it "should detect a browser type correctly" do 464 | # agents = 465 | # [ 466 | # "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0_1 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko)" 467 | # "Opera/9.61 (Windows NT 5.1; U; ru) Presto/2.1.1" 468 | # "Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)", 469 | # "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; (R1 1.5); .NET CLR 1.1.4322)", 470 | # "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.49 Safari/532.5", 471 | # "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-us) AppleWebKit/531.9 (KHTML, like Gecko) Version/4.0.3 Safari/531.9", 472 | # "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; InfoPath.2; MS-RTC LM 8; SPC 3.1 P1 Ta)", 473 | # "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7" 474 | # ] 475 | # # results = ["Chrome - 4.0.249.49", "Safari - 531.9", "MSIE 7.0", "Firefox - 3.5.7"] 476 | # # results = ["Chrome", "Safari", "MSIE 7.0", "Firefox"] 477 | # agents.each do |agent| 478 | # browser = @rack.send( :id_browser, agent ) 479 | # browser.should == results.shift 480 | # end 481 | # end 482 | # 483 | # it "should return unknow if can't detect it" do 484 | # @rack.send( :id_browser, 'IBrowse' ).should == 'N/A' 485 | # end 486 | # end 487 | 488 | # --------------------------------------------------------------------------- 489 | describe 'YAML load' do 490 | before :all do 491 | @config_file = File.join( File.dirname(__FILE__), %w[.. test_configs rackamole_test.yml] ) 492 | end 493 | 494 | it "should raise an error if the config is hosed" do 495 | flawed = File.join( File.dirname(__FILE__), %w[.. test_configs flawed_config.yml] ) 496 | lambda{ 497 | Rack::Mole.new( nil, :environment => 'test', :config_file => flawed ) 498 | }.should raise_error( RuntimeError, /Unable to parse Rackamole config file/ ) 499 | end 500 | 501 | it "should load the test env correctly from a yaml file" do 502 | @rack = Rack::Mole.new( nil, :environment => 'test', :config_file => @config_file ) 503 | @rack.send( 'options' )[:moleable].should == false 504 | end 505 | 506 | it "should load the dev env correctly from a yaml file" do 507 | @rack = Rack::Mole.new( nil, :environment => 'development', :config_file => @config_file ) 508 | opts = @rack.send( 'options' ) 509 | opts[:moleable].should == true 510 | opts[:app_name].should == 'TestApp' 511 | opts[:user_key].should == :user_name 512 | opts[:perf_threshold].should == 2 513 | 514 | @rack.send( :alertable?, :twitter, Rackamole.perf ).should == true 515 | @rack.send( :alertable?, :twitter, Rackamole.fault ).should == false 516 | @rack.send( :alertable?, :email, Rackamole.fault ).should == true 517 | @rack.send( :alertable?, :email, Rackamole.perf ).should == false 518 | end 519 | 520 | it "should load the prod env correctly" do 521 | @rack = Rack::Mole.new( nil, :environment => 'production', :config_file => @config_file ) 522 | opts = @rack.send( 'options' ) 523 | opts[:moleable].should == true 524 | opts[:app_name].should == 'TestApp' 525 | opts[:perf_threshold].should == 5 526 | (opts[:store].instance_of?(Rackamole::Store::MongoDb)).should == true 527 | opts[:store].db_name.should == "mole_fred_production" 528 | opts[:store].port.should == 10 529 | opts[:store].host.should == "fred" 530 | end 531 | end 532 | 533 | # --------------------------------------------------------------------------- 534 | describe 'excludes params' do 535 | 536 | it "should exclude request params correctly" do 537 | @opts[:param_excludes] = [:bobo] 538 | app( @opts ) 539 | get "/", { :blee => 'duh', :bobo => 10 }, @test_env 540 | params = @test_store.mole_result[:params] 541 | params.should_not be_nil 542 | params[:blee].should == 'duh'.to_json 543 | params.keys.size.should == 1 544 | params.has_key?( :bobo ).should == false 545 | end 546 | 547 | it "should exclude session params correctly" do 548 | @test_env['rack.session'][:bobo] = 'exclude_me' 549 | @opts[:session_excludes] = [:bobo] 550 | app( @opts ) 551 | get "/", { :username => 'duh', :bobo => 10 }, @test_env 552 | params = @test_store.mole_result[:params] 553 | params.should_not be_nil 554 | params.keys.size.should == 2 555 | session = @test_store.mole_result[:session] 556 | session.should_not be_nil 557 | session.keys.size.should == 2 558 | session.has_key?( :bobo ).should == false 559 | end 560 | 561 | it "should check deeply" do 562 | @opts[:param_excludes] = [:password] 563 | app( @opts ) 564 | get "/", { :blee => 'duh', :member => {:password => 'secret', :name => 'Kamui'} }, @test_env 565 | 566 | params = @test_store.mole_result[:params] 567 | params.keys.size.should == 2 568 | params[:member].should == {:name=>'Kamui'}.to_json 569 | end 570 | end 571 | 572 | # --------------------------------------------------------------------------- 573 | describe 'required params' do 574 | it "should crap out if a required param is omitted" do 575 | lambda { 576 | Rack::Mole.new( app ) 577 | }.should raise_error( /app_name/ ) 578 | end 579 | end 580 | 581 | end 582 | --------------------------------------------------------------------------------