rackamole 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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...