rackamole 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -82,7 +82,7 @@ interactions and leverage these findings for the next iteration of your applicat
82
82
 
83
83
  (The MIT License)
84
84
 
85
- Copyright (c) 2008 FIXME (different license?)
85
+ Copyright (c) 2009
86
86
 
87
87
  Permission is hereby granted, free of charge, to any person obtaining
88
88
  a copy of this software and associated documentation files (the
data/Rakefile CHANGED
@@ -28,4 +28,6 @@ PROJ.rcov.opts = ["--sort", "coverage", "-T", '-x mongo']
28
28
  depend_on "logging" , ">= 1.2.2"
29
29
  depend_on "hitimes" , ">= 1.0.3"
30
30
  depend_on "mongo" , ">= 0.17.1"
31
- depend_on "darkfish-rdoc", ">= 1.1.5"
31
+ depend_on "darkfish-rdoc", ">= 1.1.5"
32
+ depend_on "twitter4r" , ">= 0.3.0"
33
+ depend_on "actionmailer" , ">= 2.1.0"
data/aa.txt ADDED
@@ -0,0 +1,10 @@
1
+ :twitt_if => [Rackamole.feature, Rackamole.fault, Rackamole.perf],
2
+ :twitter => { :username => 'moled', :password => 'fernand~1' },
3
+
4
+ require 'action_mailer'
5
+ ActionMailer::Base.delivery_method = :sendmail
6
+ ActionMailer::Base.raise_delivery_errors = true
7
+
8
+ :email => { :from => 'fernand@collectiveintellect.com', :to => ['fernand@collectiveintellect.com'] },
9
+ :email_if => [Rackamole.feature, Rackamole.fault],
10
+
data/lib/rackamole.rb CHANGED
@@ -1,21 +1,24 @@
1
1
  module Rackamole
2
2
 
3
3
  # :stopdoc:
4
- VERSION = '0.0.6'
4
+ VERSION = '0.0.7'
5
5
  LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
6
6
  PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
7
7
  # :startdoc:
8
8
 
9
9
  # Returns the version string for the library.
10
- #
11
10
  def self.version
12
11
  VERSION
13
12
  end
14
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
+
15
19
  # Returns the library path for the module. If any arguments are given,
16
20
  # they will be joined to the end of the libray path using
17
21
  # <tt>File.join</tt>.
18
- #
19
22
  def self.libpath( *args )
20
23
  args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
21
24
  end
@@ -23,7 +26,6 @@ module Rackamole
23
26
  # Returns the lpath for the module. If any arguments are given,
24
27
  # they will be joined to the end of the path using
25
28
  # <tt>File.join</tt>.
26
- #
27
29
  def self.path( *args )
28
30
  args.empty? ? PATH : ::File.join(PATH, args.flatten)
29
31
  end
@@ -32,7 +34,6 @@ module Rackamole
32
34
  # directory below this file that has the same name as the filename passed
33
35
  # in. Optionally, a specific _directory_ name can be passed in such that
34
36
  # the _filename_ does not have to be equivalent to the directory.
35
- #
36
37
  def self.require_all_libs_relative_to( fname, dir = nil )
37
38
  dir ||= ::File.basename(fname, '.*')
38
39
  search_me = ::File.expand_path(
@@ -0,0 +1,67 @@
1
+ require 'action_mailer'
2
+
3
+ module Rackamole::Alert
4
+ class Emole < ActionMailer::Base
5
+ self.template_root = File.join( File.dirname(__FILE__), %w[templates] )
6
+
7
+ # Send an email notification for particular moled feature. An email will
8
+ # be sent based on the two configuration :emails and :mail_on defined on the
9
+ # Rack::Mole component. These specify the to and from addresses and the conditions
10
+ # that will trigger the email, currently :enabled and :features for the type of
11
+ # moled features to track via email. The notification will be sent via actionmailer,
12
+ # so you will need to make sure it is properly configured for your domain.
13
+ # NOTE: This is just a notification mechanism. All moled event will be either logged
14
+ # or persisted in the db regardless.
15
+ #
16
+ # === Parameters:
17
+ # from :: The from address address. Must be a valid domain.
18
+ # recipients :: An array of email addresses for recipients to be notified.
19
+ # args :: The gathered information from the mole.
20
+ #
21
+ def alert( from, recipients, args )
22
+ buff = []
23
+
24
+ dump( buff, args, 0 )
25
+
26
+ from from
27
+ recipients recipients
28
+ subject "[M()le] (#{alert_type( args )}#{request_time?( args )}) -#{args[:app_name]}@#{args[:host]}- for user #{args[:user_name]}"
29
+ body :args => args,
30
+ :dump => buff.join( "\n" )
31
+ end
32
+
33
+ # =========================================================================
34
+ private
35
+
36
+ # Dump request time if any...
37
+ def request_time?( args )
38
+ args[:type] == Rackamole.perf ? ":#{args[:request_time]}" : ''
39
+ end
40
+
41
+ # Identify the type of alert...
42
+ def alert_type( args )
43
+ case args[:type]
44
+ when Rackamole.feature : "Feature"
45
+ when Rackamole.perf : "Performance"
46
+ when Rackamole.fault : "Fault"
47
+ end
48
+ end
49
+
50
+ # Dump args...
51
+ def dump( buff, env, level=0 )
52
+ env.each_pair do |k,value|
53
+ if value.respond_to?(:each_pair)
54
+ buff << "%s %-#{40-level}s" % [' '*level,k]
55
+ dump( buff, env[k], level+1 )
56
+ elsif value.instance_of?(Array)
57
+ buff << "%s %-#{40-level}s" % [' '*level,k]
58
+ value.each do |v|
59
+ buff << "%s %-#{40-(level+1)}s" % [' '*(level+1),v]
60
+ end
61
+ else
62
+ buff << "%s %-#{40-level}s %s" % [ ' '*level, k, value.inspect ]
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,9 @@
1
+ A watched feature was triggered in application `<%= @args[:app_name] %> on host `<%=@args[:host]%>
2
+
3
+ Details...
4
+
5
+ <%= @dump %>
6
+
7
+ - Your Rackamole
8
+
9
+ This message was generated automatically. Please do not respond directly.
@@ -0,0 +1,73 @@
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
+ # This class is used to send out moled twitter notification. This feature is enabled
9
+ # by setting both :twitter_auth and twitt_on options on the Rack::Mole. When a moled
10
+ # feature comes around it will be twitted on your configured account. This allow your
11
+ # app to twitt about it's status and issues. Currently there are no provisions to throttle
12
+ # the twitts, hence sending out twitt notifications of every moled features would not be
13
+ # a very good idea. Whereas sending twitts when your application bogs down or throws exception,
14
+ # might be more appropriate. Further work will take place to throttle these events...
15
+ # Creating a private twitter account and asking folks in your group to follow might be an
16
+ # alternative to email.
17
+ #
18
+ # NOTE: This is just an alert mechanism. All moled events will be either logged or persisted in the db
19
+ # regardless.
20
+ #
21
+ # === Params:
22
+ # username :: The name on the twitter account
23
+ # password :: The password of your twitter account
24
+ def initialize( username, password )
25
+ raise "You must specify your twitter account credentials" unless username or password
26
+ @username = username
27
+ @password = password
28
+ end
29
+
30
+ # Send out a twitt notification based of the watched features. A short message will be blasted to your twitter
31
+ # account based on information reported by the mole. The twitt will be automatically truncated
32
+ # to 140 chars.
33
+ #
34
+ # === Params:
35
+ # args :: The moled info for a given feature.
36
+ #
37
+ def send_alert( args )
38
+ twitt_msg = "#{args[:app_name]}:#{args[:host]}\n#{args[:user_name]} : #{display_feature(args)}"
39
+ twitt_msg = case args[:type]
40
+ when Rackamole.feature : "[Feature] -- #{twitt_msg}"
41
+ when Rackamole.perf : "[Perf] -- #{twitt_msg} : #{args[:request_time]} secs"
42
+ when Rackamole.fault : "[Fault] -- #{twitt_msg} : #{args[:fault]}"
43
+ else nil
44
+ end
45
+ twitt.status( :post, truncate( twitt_msg ) ) if twitt_msg
46
+ twitt_msg
47
+ rescue => boom
48
+ $stderr.puts "TWITT mole failed with #{boom}"
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :username, :password #:nodoc:
54
+
55
+ # Fetch twitter connection...
56
+ def twitt
57
+ @twitt ||= ::Twitter::Client.new( :login => username, :password => password )
58
+ end
59
+
60
+ # Display controller/action or path depending on frmk used...
61
+ def display_feature( args )
62
+ return args[:path] unless args[:route_info]
63
+ "#{args[:route_info][:controller]}##{args[:route_info][:action]}"
64
+ end
65
+
66
+ # Truncate for twitt max size
67
+ def truncate(text, length = 140, truncate_string = "...")
68
+ return "" if text.nil?
69
+ l = length - truncate_string.mb_chars.length
70
+ text.mb_chars.length > length ? (text.mb_chars[0...l] + truncate_string).to_s : text
71
+ end
72
+ end
73
+ end
@@ -1,9 +1,15 @@
1
1
  module Rackamole
2
2
  module Interceptor
3
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
+ #
4
10
  # In order for the mole to trap framework exception, you must include
5
11
  # the interceptor in your application controller.
6
- # ie include Wackamole::Interceptor
12
+ # ie include Rackamole::Interceptor
7
13
  def self.included( base )
8
14
  base.send( :alias_method_chain, :rescue_action_in_public, :mole )
9
15
  base.send( :alias_method_chain, :rescue_action_locally, :mole )
@@ -5,7 +5,7 @@ module Rackamole
5
5
  class Logger
6
6
  class ConfigurationError < StandardError ; end #:nodoc:
7
7
 
8
- attr_reader :log # here for testing, don't really use it.
8
+ attr_reader :log #:nodoc:
9
9
 
10
10
  extend Forwardable
11
11
  def_delegators :@log, :debug, :warn, :info, :error, :fatal
@@ -47,7 +47,10 @@ module Rackamole
47
47
  }
48
48
  end
49
49
 
50
- # create a new logger
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.
51
54
  #
52
55
  def initialize( opts = {} )
53
56
  @options = ::Rackamole::Logger.default_options.merge(opts)
@@ -2,21 +2,59 @@ require 'hitimes'
2
2
  require 'json'
3
3
  require 'mongo'
4
4
 
5
+ # BOZO !! - Need args validator or use dsl as the args are out of control...
5
6
  module Rack
6
7
  class Mole
7
8
 
8
- # Initialize The Mole with the possible options
9
- # <tt>:app_name</tt> - The name of the application [Default: Moled App]
10
- # <tt>:environment</tt> - The environment for the application ie :environment => RAILS_ENV
11
- # <tt>:perf_threshold</tt> - Any request taking longer than this value will get moled [Default: 10]
12
- # <tt>:moleable</tt> - Enable/Disable the MOle [Default:true]
13
- # <tt>:store</tt> - The storage instance ie log file or mongodb [Default:stdout]
14
- # <tt>:user_key</tt> - If session is enable, the session key for the user name or user_id. ie :user_key => :user_name
9
+ # Initialize The Mole rack component. It is recommended that you specify at a minimum a user_key to track
10
+ # interactions on a per user basis. If you wish to use the mole for the same application in different
11
+ # environments you should set the environment attribute RAILS_ENV for example. The perf_threshold setting
12
+ # is also recommended to get performance notifications should your web app start sucking.
13
+ # By default your app will be moleable upon installation and you should see mole features spewing out in your
14
+ # logs. This is the default setting. Alternatively you can store mole information in a mongo database by
15
+ # specifying a different store option.
16
+ #
17
+ # === Options
18
+ #
19
+ # :app_name :: The name of the application (Default: Moled App)
20
+ # :environment :: The environment for the application ie :environment => RAILS_ENV
21
+ # :perf_threshold :: Any request taking longer than this value will get moled. Default: 10secs
22
+ # :moleable :: Enable/Disable the MOle (Default:true)
23
+ # :store :: The storage instance ie log file or mongodb [Default:stdout]
24
+ # :user_key :: If sessions are enable, this represents the session key for the user name or
25
+ # user_id.
26
+ # ==
27
+ # If the username resides in the session hash with key :user_name the you can use:
28
+ # :user_key => :user_name
29
+ # Or you can eval it on the fly - though this will be much slower and not recommended
30
+ # :user_key => { :session_key => :user_id, :extractor => lambda{ |id| User.find( id ).name} }
31
+ # ==
32
+ #
33
+ # :twitter_auth :: You can setup the MOle twit interesting events to a private (public if you indulge pain!) twitter account.
34
+ # Specified your twitter account information using a hash with :username and :password key
35
+ # :twitt_on :: You must configure your twitter auth and configuration using this hash. By default this option is disabled.
36
+ # ==
37
+ # :twitt_on => { :enabled => false, :features => [Rackamole.perf, Rackamole.fault] }
38
+ # ==
39
+ # ==== BOZO! currently there is not support for throttling or monitoring these alerts.
40
+ # ==
41
+ # :emails :: The mole can be configured to send out emails bases on interesting mole features.
42
+ # This feature uses actionmailer. You must specify a hash for the from and to options.
43
+ # ==
44
+ # :emails => { :from => 'fred@acme.com', :to => ['blee@acme.com', 'doh@acme.com'] }
45
+ # ==
46
+ # :mail_on :: Hash for email alert triggers. May be enabled or disabled per env settings. Default is disabled
47
+ # ==
48
+ # :mail_on => {:enabled => true, :features => [Rackamole.perf, Rackamole.fault] }
15
49
  def initialize( app, opts={} )
16
50
  @app = app
17
51
  init_options( opts )
52
+ validate_options
18
53
  end
19
-
54
+
55
+ # Entering the MOle zone...
56
+ # Watches incoming requests and report usage information. The mole will also track request that
57
+ # are taking longer than expected and also report any requests that are raising exceptions.
20
58
  def call( env )
21
59
  # Bail if application is not moleable
22
60
  return @app.call( env ) unless moleable?
@@ -27,27 +65,22 @@ module Rack
27
65
  status, headers, body = @app.call( env )
28
66
  rescue => boom
29
67
  env['mole.exception'] = boom
30
- @store.mole( mole_info( env, elapsed, status, headers, body ) )
68
+ mole_feature( env, elapsed, status, headers, body )
31
69
  raise boom
32
70
  end
33
71
  end
34
- @store.mole( mole_info( env, elapsed, status, headers, body ) )
72
+ mole_feature( env, elapsed, status, headers, body )
35
73
  return status, headers, body
36
74
  end
37
75
 
38
76
  # ===========================================================================
39
77
  private
40
78
 
79
+ attr_reader :options #:nodoc:
80
+
41
81
  # Load up configuration options
42
82
  def init_options( opts )
43
- options = default_options.merge( opts )
44
- @environment = options[:environment]
45
- @perf_threshold = options[:perf_threshold]
46
- @moleable = options[:moleable]
47
- @app_name = options[:app_name]
48
- @user_key = options[:user_key]
49
- @store = options[:store]
50
- @excluded_paths = options[:excluded_paths]
83
+ @options = default_options.merge( opts )
51
84
  end
52
85
 
53
86
  # Mole default options
@@ -57,13 +90,61 @@ module Rack
57
90
  :excluded_paths => [/.?\.ico/, /.?\.png/],
58
91
  :moleable => true,
59
92
  :perf_threshold => 10,
60
- :store => Rackamole::Store::Log.new
93
+ :store => Rackamole::Store::Log.new,
94
+ :twitt_on => { :enabled => false, :features => [Rackamole.perf, Rackamole.fault] },
95
+ :mail_on => { :enabled => false, :features => [Rackamole.perf, Rackamole.fault] }
61
96
  }
62
97
  end
63
-
98
+
99
+ # Validates all configured options... Throws error if invalid configuration
100
+ def validate_options
101
+ %w[app_name moleable perf_threshold store].each do |k|
102
+ raise "[M()le] -- Unable to locate required option key `#{k}" unless options[k.to_sym]
103
+ end
104
+ end
105
+
106
+ # Send moled info to store and potentially send out alerts...
107
+ def mole_feature( env, elapsed, status, headers, body )
108
+ attrs = mole_info( env, elapsed, status, headers, body )
109
+
110
+ # send info to configured store
111
+ options[:store].mole( attrs )
112
+
113
+ # send email alert ?
114
+ if configured?( :emails, [:from, :to] ) and alertable?( options[:mail_on], attrs[:type] )
115
+ Rackamole::Alert::Emole.deliver_alert( options[:emails][:from], options[:emails][:to], attrs )
116
+ end
117
+
118
+ # send twitter alert ?
119
+ if configured?( :twitter_auth, [:username, :password] ) and alertable?( options[:twitt_on], attrs[:type] )
120
+ twitt.send_alert( attrs )
121
+ end
122
+ rescue => boom
123
+ $stderr.puts "!! MOLE RECORDING CRAPPED OUT !! -- #{boom}"
124
+ boom.backtrace.each { |l| $stderr.puts l }
125
+ end
126
+
127
+ # Check if an options is set and configured
128
+ def configured?( key, configs )
129
+ return false unless options[key]
130
+ configs.each { |c| return false unless options[key][c] }
131
+ true
132
+ end
133
+
134
+ # Check if feature should be send to alert clients ie email or twitter
135
+ def alertable?( filters, type )
136
+ return false if !filters or filters.empty? or !filters[:enabled]
137
+ filters[:features].include?( type )
138
+ end
139
+
140
+ # Create or retrieve twitter client
141
+ def twitt
142
+ @twitt ||= Rackamole::Alert::Twitt.new( options[:twitter_auth][:username], options[:twitter_auth][:password] )
143
+ end
144
+
64
145
  # Check if this request should be moled according to the exclude filters
65
146
  def mole_request?( request )
66
- @excluded_paths.each do |exclude_path|
147
+ options[:excluded_paths].each do |exclude_path|
67
148
  return false if request.path.match( exclude_path )
68
149
  end
69
150
  true
@@ -87,19 +168,21 @@ module Rack
87
168
 
88
169
  # BOZO !! This could be slow if have to query db to get user name...
89
170
  # Preferred store username in session and give at key
90
- if session and @user_key
91
- if @user_key.instance_of? Hash
92
- user_id = session[ @user_key[:session_key] ]
93
- if @user_key[:extractor]
94
- user_name = @user_key[:extractor].call( user_id )
171
+ user_key = options[:user_key]
172
+ if session and user_key
173
+ if user_key.instance_of? Hash
174
+ user_id = session[ user_key[:session_key] ]
175
+ if user_key[:extractor]
176
+ user_name = user_key[:extractor].call( user_id )
95
177
  end
96
178
  else
97
- user_name = session[@user_key]
179
+ user_name = session[user_key]
98
180
  end
99
181
  end
100
-
101
- info[:app_name] = @app_name
102
- info[:environment] = @environment || "Unknown"
182
+
183
+ info[:type] = (elapsed and elapsed > options[:perf_threshold] ? Rackamole.perf : Rackamole.feature)
184
+ info[:app_name] = options[:app_name]
185
+ info[:environment] = options[:environment] || "Unknown"
103
186
  info[:user_id] = user_id if user_id
104
187
  info[:user_name] = user_name || "Unknown"
105
188
  info[:ip] = ip
@@ -107,7 +190,6 @@ module Rack
107
190
  info[:host] = env['SERVER_NAME']
108
191
  info[:software] = env['SERVER_SOFTWARE']
109
192
  info[:request_time] = elapsed if elapsed
110
- info[:performance] = (elapsed and elapsed > @perf_threshold)
111
193
  info[:url] = request.url
112
194
  info[:method] = env['REQUEST_METHOD']
113
195
  info[:path] = request.path
@@ -129,13 +211,12 @@ module Rack
129
211
  exception = env['mole.exception']
130
212
  if exception
131
213
  info[:ruby_version] = %x[ruby -v]
214
+ info[:fault] = exception.to_s
132
215
  info[:stack] = trim_stack( exception )
216
+ info[:type] = Rackamole.fault
133
217
  env['mole.exception'] = nil
134
218
  end
135
219
  info
136
- rescue => boom
137
- $stderr.puts "!! MOLE RECORDING CRAPPED OUT !! -- #{boom}"
138
- boom.backtrace.each { |l| $stderr.puts l }
139
220
  end
140
221
 
141
222
  # Attempts to detect browser type from agent info.
@@ -162,7 +243,7 @@ module Rack
162
243
 
163
244
  # Checks if this application is moleable
164
245
  def moleable?
165
- @moleable
246
+ options[:moleable]
166
247
  end
167
248
 
168
249
  # Fetch route info if any...