exception_handling 0.1.1

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/.gitignore ADDED
@@ -0,0 +1 @@
1
+ .idea
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # ruby '1.8.7'
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in attr_default.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,79 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ exception_handling (0.1.1)
5
+ actionmailer (= 3.2.13)
6
+ actionpack (= 3.2.13)
7
+ activesupport (= 3.2.13)
8
+ eventmachine (>= 0.12.10)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ actionmailer (3.2.13)
14
+ actionpack (= 3.2.13)
15
+ mail (~> 2.5.3)
16
+ actionpack (3.2.13)
17
+ activemodel (= 3.2.13)
18
+ activesupport (= 3.2.13)
19
+ builder (~> 3.0.0)
20
+ erubis (~> 2.7.0)
21
+ journey (~> 1.0.4)
22
+ rack (~> 1.4.5)
23
+ rack-cache (~> 1.2)
24
+ rack-test (~> 0.6.1)
25
+ sprockets (~> 2.2.1)
26
+ activemodel (3.2.13)
27
+ activesupport (= 3.2.13)
28
+ builder (~> 3.0.0)
29
+ activesupport (3.2.13)
30
+ i18n (= 0.6.1)
31
+ multi_json (~> 1.0)
32
+ bourne (1.3.0)
33
+ mocha (= 0.13.0)
34
+ builder (3.0.4)
35
+ erubis (2.7.0)
36
+ eventmachine (1.0.3)
37
+ hike (1.2.3)
38
+ i18n (0.6.1)
39
+ journey (1.0.4)
40
+ mail (2.5.4)
41
+ mime-types (~> 1.16)
42
+ treetop (~> 1.4.8)
43
+ metaclass (0.0.1)
44
+ mime-types (1.24)
45
+ mocha (0.13.0)
46
+ metaclass (~> 0.0.1)
47
+ multi_json (1.7.8)
48
+ polyglot (0.3.3)
49
+ rack (1.4.5)
50
+ rack-cache (1.2)
51
+ rack (>= 0.4)
52
+ rack-test (0.6.2)
53
+ rack (>= 1.0)
54
+ rake (10.1.0)
55
+ shoulda (3.1.1)
56
+ shoulda-context (~> 1.0)
57
+ shoulda-matchers (~> 1.2)
58
+ shoulda-context (1.1.4)
59
+ shoulda-matchers (1.5.6)
60
+ activesupport (>= 3.0.0)
61
+ bourne (~> 1.3)
62
+ sprockets (2.2.2)
63
+ hike (~> 1.2)
64
+ multi_json (~> 1.0)
65
+ rack (~> 1.0)
66
+ tilt (~> 1.1, != 1.3.0)
67
+ tilt (1.4.1)
68
+ treetop (1.4.15)
69
+ polyglot
70
+ polyglot (>= 0.3.1)
71
+
72
+ PLATFORMS
73
+ ruby
74
+
75
+ DEPENDENCIES
76
+ exception_handling!
77
+ mocha (= 0.13.0)
78
+ rake (>= 0.9)
79
+ shoulda (= 3.1.1)
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Colin Kelley, RingRevenue
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1 @@
1
+ exception_handling gem
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ task :default => :test
5
+
6
+ desc "Run unit tests."
7
+ task :test do
8
+ ruby "test/exception_handling_test.rb"
9
+ end
@@ -0,0 +1,138 @@
1
+ --- !map:HashWithIndifferentAccess
2
+ All script kiddies:
3
+ error: "ScriptKiddie suspected because of HTTP request without a referer"
4
+
5
+ Tony Robbins:
6
+ request: " url: https?\:\/\/www\.tonyrobbins\.com"
7
+
8
+ Soulful Beauty:
9
+ request: "HTTP_REFERER: http://.*soulfulbeauty\.com"
10
+
11
+ The Credit Exchange:
12
+ error: "VirtualLine.find_by_external_ids - No promo number found.*Affiliate id from network (1389811|3079560|3366822|3179522|3375281|3375756|3362634|550151) not found"
13
+ request: "av_id: 178$"
14
+
15
+ Spiders:
16
+ environment: "Microsoft-WebDAV|COMODOspider|Baiduspider|YandexBot|TurnitinBot"
17
+
18
+ Googlebot:
19
+ environment: "googlebot"
20
+ error: "ReferenceError: Can't find variable: RingRevenue"
21
+
22
+ Options Request:
23
+ environment: "REQUEST_METHOD: OPTIONS"
24
+
25
+ Click Request Rejected:
26
+ error: "Request to click domain rejected"
27
+ request: "controller: corporate"
28
+
29
+ Staging Full Started:
30
+ error: "Mysql::Error: Can't connect to MySQL server on 'stagingmaster.ringrevenue.com'"
31
+
32
+ Google Analytics:
33
+ error: "ActionController.*RoutingError*google-analytics.*"
34
+
35
+ TermsLinksBroken:
36
+ error: "Logged in user experienced broken link on.*advertiser_campaign_terms"
37
+
38
+ NoRouteOnTerms:
39
+ error: "ActionController.*RoutingError.*No route matches.*campaign_terms"
40
+
41
+ InvalidAuth:
42
+ error: "Invalid authenticity token received from.*new\?"
43
+
44
+ NoRoute:
45
+ error: "No route matches"
46
+
47
+ SearchWebPlacementsJson:
48
+ error: "search_web_placement Invalid JSON response from yahoo api"
49
+
50
+ Rss 404s from Java:
51
+ request: "controller: articles"
52
+ error: "ActionController.*UnknownAction"
53
+ environment: "HTTP_USER_AGENT: Java"
54
+
55
+ Archiver mangling caused missing template:
56
+ error: "Missing template corporate/platform.erb"
57
+ environment: "(aihit|archive)"
58
+
59
+ Number Loader 404s from invalid url concatenation:
60
+ request: "url: https?:\/\/js\d{0,2}\.ringrevenue.com\/"
61
+ error: "Broken link.*ActiveRecord::RecordNotFound"
62
+
63
+ collegequest bad TLD:
64
+ error: "Hostname 'me.cq' does not have a known public suffix"
65
+
66
+ Ignite calls API:
67
+ request: "calls\/10\.xml"
68
+
69
+ BrokenLinkAfterLogin:
70
+ error: "Found broken link after user logged in from"
71
+
72
+ Link Trust Pixel Firing:
73
+ error: "Errno::ECONNREFUSED.*(linktrust\.com|mobitracking\.com|xy7track\.com|mbltrack\.com)"
74
+
75
+ # JAVASCRIPT exceptions
76
+ # http://www.roslindesign.com/2010/10/12/avg-antivirus-2011-corrupting-web-pages-with-injection-of-script-avg_ls_dom-js/
77
+ AVG Anti Virus:
78
+ request: "avg_ls_dom\.js"
79
+
80
+ Firefox Framework Bug:
81
+ error: "Javascript: window.onerror: Permission denied to access property 'nodeType' from a non-chrome context"
82
+ request: "action: javascript_error"
83
+
84
+ Javascript Error Loading Script:
85
+ error: "Javascript: window.onerror: Error loading script"
86
+ request: "filename: .*hubspot\.com.*"
87
+
88
+ Javascript Plugin Errors:
89
+ error: "Javascript: window.onerror: Script error"
90
+ request: "filename: chrome://leapfrog.*"
91
+
92
+ Javascript users with browser plugin issues:
93
+ error: "Javascript: .*"
94
+ session: "user_id: (42595|58296|61276|74059)"
95
+
96
+ Calls API Errors:
97
+ error: "calls_api returning status not_found"
98
+
99
+ Duplicate Sales Reported:
100
+ error: "Call.update_order called with duplicate sales"
101
+
102
+ Sale for the same reason and sku:
103
+ error: "Sale for the same reason and sku"
104
+
105
+ Found Inconsistent Caller Id Settings:
106
+ error: "CampaignIds:CampaignTerms_7542,CampaignTerms_7544,CampaignTerms_8242,CampaignTerms_8478,CampaignTerms_9244"
107
+
108
+ Invalid tacked_action referrer:
109
+ error: "StandardError: process_click_from_network_id - saving click failed.: Mysql::Error: Incorrect string value:.*for column 'referrer'.*INSERT INTO `tracked_actions`"
110
+
111
+ invalid click domain rdparking:
112
+ error: "Request to click domain rejected"
113
+ request: ".*rdparking.*"
114
+
115
+ update order with duplicate sales:
116
+ error: "update_order called with duplicate sales information"
117
+
118
+ failsafe on vxml because readonly:
119
+ error: "The MySQL server is running with the --read-only option so it cannot execute this statement.*INSERT INTO simple_sessions"
120
+
121
+ failsafe on propfind:
122
+ error: "UnknownHttpMethod: PROPFIND"
123
+
124
+ pixel_misconfigured:
125
+ error: "PixelUrlTemplate substitution"
126
+
127
+ buffered_sale:
128
+ error: "Couldn't apply BufferedSale"
129
+
130
+ SQL Injection Attempts:
131
+ request: "controller: virtual_lines"
132
+ request: "av_id: \D\d*\D\d*\D\d*\D"
133
+
134
+ Opera Browser:
135
+ environment: "HTTP_USER_AGENT: Opera"
136
+
137
+ Affiliate Map Number invalid requests:
138
+ error: "Publisher Map Number returning with invalid campaign"
@@ -0,0 +1,24 @@
1
+ require File.expand_path('../lib/exception_handling/version', __FILE__)
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.add_dependency 'eventmachine', '>=0.12.10'
5
+ gem.add_dependency 'activesupport', '3.2.13'
6
+ gem.add_dependency 'actionpack', '3.2.13'
7
+ gem.add_dependency 'actionmailer', '3.2.13'
8
+ gem.add_development_dependency 'rake', '>=0.9'
9
+ gem.add_development_dependency 'shoulda', '=3.1.1'
10
+ gem.add_development_dependency 'mocha', '=0.13.0'
11
+
12
+ gem.authors = ["Colin Kelley"]
13
+ gem.email = ["colindkelley@gmail.com"]
14
+ gem.description = %q{Exception handling logger/emailer}
15
+ gem.summary = %q{RingRevenue's exception handling logger/emailer layer, based on exception_notifier. Works with Rails or EventMachine or EventMachine+Synchrony.}
16
+ gem.homepage = "https://github.com/RingRevenue/exception_handling"
17
+
18
+ gem.files = `git ls-files`.split($\)
19
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/.*\.rb})
21
+ gem.name = "exception_handling"
22
+ gem.require_paths = ["lib"]
23
+ gem.version = ExceptionHandling::VERSION
24
+ end
@@ -0,0 +1,3 @@
1
+ module ExceptionHandling
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,630 @@
1
+ require 'timeout'
2
+ require 'active_support'
3
+ require 'active_support/core_ext/hash'
4
+ require "#{File.dirname(__FILE__)}/exception_handling_mailer"
5
+
6
+ EXCEPTION_HANDLING_MAILER_SEND_MAIL = true unless defined?(EXCEPTION_HANDLING_MAILER_SEND_MAIL)
7
+
8
+ _ = ActiveSupport::HashWithIndifferentAccess
9
+
10
+ if defined?(EVENTMACHINE_EXCEPTION_HANDLING) && EVENTMACHINE_EXCEPTION_HANDLING
11
+ require 'em/protocols/smtpclient'
12
+ end
13
+
14
+ module ExceptionHandling # never included
15
+
16
+ class Warning < StandardError; end
17
+ class MailerTimeout < Timeout::Error; end
18
+ class ClientLoggingError < StandardError; end
19
+
20
+ SUMMARY_THRESHOLD = 5
21
+ SUMMARY_PERIOD = 60*60 # 1.hour
22
+
23
+
24
+ SECTIONS = [:request, :session, :environment, :backtrace, :event_response]
25
+ EXCEPTION_FILTER_LIST_PATH = "#{defined?(Rails) ? Rails.root : '.'}/config/exception_filters.yml"
26
+
27
+ ENVIRONMENT_WHITELIST = [
28
+ /^HTTP_/,
29
+ /^QUERY_/,
30
+ /^REQUEST_/,
31
+ /^SERVER_/
32
+ ]
33
+
34
+ ENVIRONMENT_OMIT =(
35
+ <<EOF
36
+ CONTENT_TYPE: application/x-www-form-urlencoded
37
+ GATEWAY_INTERFACE: CGI/1.2
38
+ HTTP_ACCEPT: */*
39
+ HTTP_ACCEPT: */*, text/javascript, text/html, application/xml, text/xml, */*
40
+ HTTP_ACCEPT_CHARSET: ISO-8859-1,utf-8;q=0.7,*;q=0.7
41
+ HTTP_ACCEPT_ENCODING: gzip, deflate
42
+ HTTP_ACCEPT_ENCODING: gzip,deflate
43
+ HTTP_ACCEPT_LANGUAGE: en-us
44
+ HTTP_CACHE_CONTROL: no-cache
45
+ HTTP_CONNECTION: Keep-Alive
46
+ HTTP_HOST: www.ringrevenue.com
47
+ HTTP_MAX_FORWARDS: 10
48
+ HTTP_UA_CPU: x86
49
+ HTTP_VERSION: HTTP/1.1
50
+ HTTP_X_FORWARDED_HOST: www.ringrevenue.com
51
+ HTTP_X_FORWARDED_SERVER: www2.ringrevenue.com
52
+ HTTP_X_REQUESTED_WITH: XMLHttpRequest
53
+ LANG:
54
+ LANG:
55
+ PATH: /sbin:/usr/sbin:/bin:/usr/bin
56
+ PWD: /
57
+ RAILS_ENV: production
58
+ RAW_POST_DATA: id=500
59
+ REMOTE_ADDR: 10.251.34.225
60
+ SCRIPT_NAME: /
61
+ SERVER_NAME: www.ringrevenue.com
62
+ SERVER_PORT: 80
63
+ SERVER_PROTOCOL: HTTP/1.1
64
+ SERVER_SOFTWARE: Mongrel 1.1.4
65
+ SHLVL: 1
66
+ TERM: linux
67
+ TERM: xterm-color
68
+ _: /usr/bin/mongrel_cluster_ctl
69
+ EOF
70
+ ).split("\n")
71
+
72
+ AUTHENTICATION_HEADERS = ['HTTP_AUTHORIZATION','X-HTTP_AUTHORIZATION','X_HTTP_AUTHORIZATION','REDIRECT_X_HTTP_AUTHORIZATION']
73
+
74
+ @logger = Rails.logger if defined?(Rails)
75
+
76
+
77
+ class << self
78
+ attr_accessor :email_environment
79
+ attr_accessor :server_name
80
+ attr_accessor :sender_address
81
+ attr_accessor :exception_recipients
82
+
83
+ attr_accessor :current_controller
84
+ attr_accessor :last_exception_timestamp
85
+ attr_accessor :periodic_exception_intervals
86
+ attr_accessor :logger
87
+
88
+ #
89
+ # Gets called by Rack Middleware: DebugExceptions or ShowExceptions
90
+ # it does 2 things:
91
+ # log the error
92
+ # email the error
93
+ #
94
+ # but not during functional tests, when rack middleware is not used
95
+ #
96
+ def log_error_rack(exception, env, rack_filter)
97
+ timestamp = set_log_error_timestamp
98
+ exception_data = exception_to_data(exception, env, timestamp)
99
+
100
+ # TODO: add a more interesting custom description, like:
101
+ # custom_description = ": caught and processed by Rack middleware filter #{rack_filter}"
102
+ # which would be nice, but would also require changing quite a few tests
103
+ custom_description = ""
104
+ write_exception_to_log(exception, custom_description, timestamp)
105
+
106
+ if should_send_email?
107
+ controller = env['action_controller.instance']
108
+ # controller may not exist in some cases (like most 404 errors)
109
+ extract_and_merge_controller_data(controller, exception_data) if controller
110
+ log_error_email(exception_data, exception)
111
+ end
112
+ end
113
+
114
+ #
115
+ # Normal Operation:
116
+ # Called directly by our code, usually from rescue blocks.
117
+ # Does two things: write to log file and send an email
118
+ #
119
+ # Functional Test Operation:
120
+ # Calls into handle_stub_log_error and returns. no log file. no email.
121
+ #
122
+ def log_error(exception_or_string, exception_context = '', controller = nil, treat_as_local = false)
123
+ begin
124
+ ex = make_exception(exception_or_string)
125
+ timestamp = set_log_error_timestamp
126
+ data = exception_to_data(ex, exception_context, timestamp)
127
+
128
+ write_exception_to_log(ex, exception_context, timestamp)
129
+
130
+ if treat_as_local
131
+ return
132
+ end
133
+
134
+ if should_send_email?
135
+ controller ||= current_controller
136
+
137
+ if block_given?
138
+ # the expectation is that if the caller passed a block then they will be
139
+ # doing their own merge of hash values into data
140
+ begin
141
+ yield data
142
+ rescue Exception => ex
143
+ data.merge!(:environment => "Exception in yield: #{ex.class}:#{ex}")
144
+ end
145
+ elsif controller
146
+ # most of the time though, this method will not get passed a block
147
+ # and additional hash data is extracted from the controller
148
+ extract_and_merge_controller_data(controller, data)
149
+ end
150
+
151
+ log_error_email(data, ex)
152
+ end
153
+
154
+ rescue Exception => ex
155
+ $stderr.puts("ExceptionHandling.log_error rescued exception while logging #{exception_context}: #{exception_or_string}:\n#{ex.class}: #{ex}\n#{ex.backtrace.join("\n")}")
156
+ write_exception_to_log(ex, "ExceptionHandling.log_error rescued exception while logging #{exception_context}: #{exception_or_string}", timestamp)
157
+ end
158
+ end
159
+
160
+ #
161
+ # Write an exception out to the log file using our own custom format.
162
+ #
163
+ def write_exception_to_log(ex, exception_context, timestamp)
164
+ ActiveSupport::Deprecation.silence do
165
+ ExceptionHandling.logger.fatal(
166
+ if ActionView::TemplateError === ex
167
+ "#{ex} Error:#{timestamp}"
168
+ else
169
+ "\n(Error:#{timestamp}) #{ex.class} #{exception_context} (#{ex.message}):\n " + clean_backtrace(ex).join("\n ") + "\n\n"
170
+ end
171
+ )
172
+ end
173
+ end
174
+
175
+ #
176
+ # Pull certain fields out of the controller and add to the data hash.
177
+ #
178
+ def extract_and_merge_controller_data(controller, data)
179
+ data[:request] = {
180
+ :params => controller.request.parameters.to_hash,
181
+ :rails_root => Rails.root,
182
+ :url => controller.complete_request_uri
183
+ }
184
+ data[:environment].merge!(controller.request.env.to_hash)
185
+
186
+ controller.session[:fault_in_session]
187
+ data[:session] = {
188
+ :key => controller.request.session_options[:id],
189
+ :data => controller.session.dup
190
+ }
191
+ end
192
+
193
+ def log_warning( message )
194
+ log_error( Warning.new(message) )
195
+ end
196
+
197
+ def log_info( message )
198
+ ExceptionHandling.logger.info( message )
199
+ end
200
+
201
+ def log_debug( message )
202
+ ExceptionHandling.logger.debug( message )
203
+ end
204
+
205
+ def ensure_safe( exception_context = "" )
206
+ yield
207
+ rescue => ex
208
+ log_error ex, exception_context
209
+ return nil
210
+ end
211
+
212
+ def ensure_escalation( email_subject )
213
+ begin
214
+ yield
215
+ rescue => ex
216
+ log_error ex
217
+ escalate(email_subject, ex, ExceptionHandling.last_exception_timestamp)
218
+ nil
219
+ end
220
+ end
221
+
222
+ def set_log_error_timestamp
223
+ ExceptionHandling.last_exception_timestamp = Time.now.to_i
224
+ end
225
+
226
+ def should_send_email?
227
+ defined?( EXCEPTION_HANDLING_MAILER_SEND_MAIL ) && EXCEPTION_HANDLING_MAILER_SEND_MAIL
228
+ end
229
+
230
+ def trace_timing(description)
231
+ result = nil
232
+ time = Benchmark.measure do
233
+ result = yield
234
+ end
235
+ log_info "#{description} %.4fs " % time.real
236
+ result
237
+ end
238
+
239
+ def log_periodically(exception_key, interval, message)
240
+ self.periodic_exception_intervals ||= {}
241
+ last_logged = self.periodic_exception_intervals[exception_key]
242
+ if !last_logged || ( (last_logged + interval) < Time.now )
243
+ log_error( message )
244
+ self.periodic_exception_intervals[exception_key] = Time.now
245
+ end
246
+ end
247
+
248
+ # TODO: fix test to not use this.
249
+ def enhance_exception_data(data)
250
+
251
+ end
252
+
253
+ private
254
+
255
+ def log_error_email( data, exc )
256
+ enhance_exception_data( data )
257
+ normalize_exception_data( data )
258
+ clean_exception_data( data )
259
+
260
+ SECTIONS.each { |section| add_to_s( data[section] ) if data[section].is_a? Hash }
261
+
262
+ if exception_filters.filtered?( data )
263
+ return
264
+ end
265
+
266
+ if summarize_exception( data ) == :Summarized
267
+ return
268
+ end
269
+
270
+ deliver(ExceptionHandling::Mailer.exception_notification(data))
271
+
272
+ if defined?(Errplane)
273
+ Errplane.transmit(exc, :custom_data => data) unless exc.is_a?(Warning)
274
+ end
275
+
276
+ nil
277
+ end
278
+
279
+ def escalate( email_subject, ex, timestamp )
280
+ data = exception_to_data( ex, nil, timestamp )
281
+ deliver(ExceptionHandling::Mailer.escalation_notification(email_subject, data))
282
+ end
283
+
284
+ def deliver(mail_object)
285
+ if defined?(EVENTMACHINE_EXCEPTION_HANDLING) && EVENTMACHINE_EXCEPTION_HANDLING
286
+ EventMachine.schedule do # in case we're running outside the reactor
287
+ async_send_method = EVENTMACHINE_EXCEPTION_HANDLING == :Synchrony ? :asend : :send
288
+ smtp_settings = ActionMailer::Base.smtp_settings
289
+ send_deferrable = EventMachine::Protocols::SmtpClient.__send__(
290
+ async_send_method,
291
+ {
292
+ :host => smtp_settings[:address],
293
+ :port => smtp_settings[:port],
294
+ :domain => smtp_settings[:domain],
295
+ :auth => {:type=>:plain, :username=>smtp_settings[:user_name], :password=>smtp_settings[:password]},
296
+ :from => mail_object['from'].to_s,
297
+ :to => mail_object['to'].to_s,
298
+ :body => mail_object.body.to_s
299
+ }
300
+ )
301
+ send_deferrable.errback { |err| ExceptionHandling.logger.fatal("Failed to email by SMTP: #{err.inspect}") }
302
+ end
303
+ else
304
+ safe_email_deliver do
305
+ mail_object.deliver
306
+ end
307
+ end
308
+ end
309
+
310
+ def safe_email_deliver
311
+ Timeout::timeout 30, MailerTimeout do
312
+ yield
313
+ end
314
+ rescue StandardError, MailerTimeout => ex
315
+ $stderr.puts("ExceptionHandling::safe_email_deliver rescued: #{ex.class}: #{ex}\n#{ex.backtrace.join("\n")}")
316
+ log_error( ex, "ExceptionHandling::safe_email_deliver", nil, true )
317
+ end
318
+
319
+ def clean_exception_data( data )
320
+ if (as_array = data[:backtrace].to_a).size == 1
321
+ data[:backtrace] = as_array.first.to_s.split(/\n\s*/)
322
+ end
323
+
324
+ if data[:request].is_a?(Hash) && data[:request][:params].is_a?(Hash)
325
+ data[:request][:params] = clean_params(data[:request][:params])
326
+ end
327
+
328
+ if data[:environment].is_a?(Hash)
329
+ data[:environment] = clean_environment(data[:environment])
330
+ end
331
+ end
332
+
333
+ def normalize_exception_data( data )
334
+ if data[:location].nil?
335
+ data[:location] = {}
336
+ if data[:request] && data[:request].key?( :params )
337
+ data[:location][:controller] = data[:request][:params]['controller']
338
+ data[:location][:action] = "fake action"
339
+ end
340
+ end
341
+ if data[:backtrace] && data[:backtrace].first
342
+ first_line = data[:backtrace].first
343
+
344
+ # template exceptions have the line number and filename as the first element in backtrace
345
+ if matched = first_line.match( /on line #(\d*) of (.*)/i )
346
+ backtrace_hash = {}
347
+ backtrace_hash[:line] = matched[1]
348
+ backtrace_hash[:file] = matched[2]
349
+ else
350
+ backtrace_hash = Hash[* [:file, :line].zip( first_line.split( ':' )[0..1]).flatten ]
351
+ end
352
+
353
+ data[:location].merge!( backtrace_hash )
354
+ end
355
+ end
356
+
357
+ def clean_params params
358
+ params.each do |k, v|
359
+ params[k] = "[FILTERED]" if k =~ /password/
360
+ end
361
+ end
362
+
363
+ def clean_environment env
364
+ Hash[ env.map do |k, v|
365
+ [k, v] if !"#{k}: #{v}".in?(ENVIRONMENT_OMIT) && ENVIRONMENT_WHITELIST.any? { |regex| k =~ regex }
366
+ end.compact ]
367
+ end
368
+
369
+ def exception_filters
370
+ @exception_filters ||= ExceptionFilters.new( EXCEPTION_FILTER_LIST_PATH )
371
+ end
372
+
373
+ def clean_backtrace(exception)
374
+ if exception.backtrace.nil?
375
+ ['<no backtrace>']
376
+ elsif exception.is_a?(ClientLoggingError)
377
+ exception.backtrace
378
+ elsif defined?(Rails)
379
+ Rails.backtrace_cleaner.clean(exception.backtrace)
380
+ else
381
+ exception.backtrace
382
+ end
383
+ end
384
+
385
+ def clear_exception_summary
386
+ @last_exception = nil
387
+ end
388
+
389
+ # Returns :Summarized iff exception has been added to summary and therefore should not be sent.
390
+ def summarize_exception( data )
391
+ if @last_exception
392
+ same_signature = @last_exception[:backtrace] == data[:backtrace]
393
+
394
+ case @last_exception[:state]
395
+
396
+ when :NotSummarized
397
+ if same_signature
398
+ @last_exception[:count] += 1
399
+ if @last_exception[:count] >= SUMMARY_THRESHOLD
400
+ @last_exception.merge! :state => :Summarized, :first_seen => Time.now, :count => 0
401
+ end
402
+ return nil
403
+ end
404
+
405
+ when :Summarized
406
+ if same_signature
407
+ @last_exception[:count] += 1
408
+ if Time.now - @last_exception[:first_seen] > SUMMARY_PERIOD
409
+ send_exception_summary(data, @last_exception[:first_seen], @last_exception[:count])
410
+ @last_exception.merge! :first_seen => Time.now, :count => 0
411
+ end
412
+ return :Summarized
413
+ elsif @last_exception[:count] > 0 # send the left-over, if any
414
+ send_exception_summary(@last_exception[:data], @last_exception[:first_seen], @last_exception[:count])
415
+ end
416
+
417
+ else
418
+ raise "Unknown state #{@last_exception[:state]}"
419
+ end
420
+ end
421
+
422
+ # New signature we haven't seen before. Not summarized yet--we're just starting the count.
423
+ @last_exception = {
424
+ :data => data,
425
+ :count => 1,
426
+ :first_seen => Time.now,
427
+ :backtrace => data[:backtrace],
428
+ :state => :NotSummarized
429
+ }
430
+ nil
431
+ end
432
+
433
+ def send_exception_summary( exception_data, first_seen, occurrences )
434
+ Timeout::timeout 30, MailerTimeout do
435
+ deliver(ExceptionHandling::Mailer.exception_notification(exception_data, first_seen, occurrences))
436
+ end
437
+ rescue StandardError, MailerTimeout => ex
438
+ $stderr.puts("ExceptionHandling.log_error_email rescued exception while logging #{exception_context}: #{exception_or_string}:\n#{ex.class}: #{ex}\n#{ex.backtrace.join("\n")}")
439
+ log_error(ex, "ExceptionHandling::log_error_email rescued exception while logging #{exception_context}: #{exception_or_string}", nil, true)
440
+ end
441
+
442
+ def add_to_s( data_section )
443
+ data_section[:to_s] = dump_hash( data_section )
444
+ end
445
+
446
+ def exception_to_data( exception, exception_context, timestamp )
447
+ data = ActiveSupport::HashWithIndifferentAccess.new
448
+ data[:error_class] = exception.class.name
449
+ data[:error_string]= "#{data[:error_class]}: #{exception.message}"
450
+ data[:timestamp] = timestamp
451
+ data[:backtrace] = clean_backtrace(exception)
452
+ if exception_context && exception_context.is_a?(Hash)
453
+ # if we are a hash, then we got called from the DebugExceptions rack middleware filter
454
+ # and we need to do some things different to get the info we want
455
+ data[:error] = "#{data[:error_class]}: #{exception.message}"
456
+ data[:session] = exception_context['rack.session']
457
+ data[:environment] = exception_context
458
+ else
459
+ data[:error] = "#{data[:error_string]}#{': ' + exception_context unless exception_context.blank?}"
460
+ data[:environment] = { :message => exception_context }
461
+ end
462
+ data
463
+ end
464
+
465
+ def make_exception(exception_or_string)
466
+ if exception_or_string.is_a?(Exception)
467
+ exception_or_string
468
+ else
469
+ begin
470
+ # raise to capture a backtrace
471
+ raise StandardError, exception_or_string
472
+ rescue => ex
473
+ ex
474
+ end
475
+ end
476
+ end
477
+
478
+ def dump_hash( h, indent_level = 0 )
479
+ result = ""
480
+ h.sort { |a, b| a.to_s <=> b.to_s }.each do |key, value|
481
+ result << ' ' * (2 * indent_level)
482
+ result << "#{key}:"
483
+ case value
484
+ when Hash
485
+ result << "\n" << dump_hash( value, indent_level + 1 )
486
+ else
487
+ result << " #{value}\n"
488
+ end
489
+ end unless h.nil?
490
+ result
491
+ end
492
+ end
493
+
494
+ class ExceptionFilters
495
+ class Filter
496
+ def initialize filter_name, regexes
497
+ @regexes = Hash[ regexes.map do |section, regex|
498
+ section = section.to_sym
499
+ raise "Unknown section: #{section}" unless section == :error || section.in?( ExceptionHandling::SECTIONS )
500
+ [section, (Regexp.new(regex, 'i') unless regex.blank?)]
501
+ end ]
502
+
503
+ raise "Filter #{filter_name} has all blank regexes: #{regexes.inspect}" if @regexes.all? { |section, regex| regex.nil? }
504
+ end
505
+
506
+ def match?(exception_data)
507
+ @regexes.all? do |section, regex|
508
+ regex.nil? ||
509
+ case exception_data[section]
510
+ when String
511
+ regex =~ exception_data[section]
512
+ when Array
513
+ exception_data[section].any? { |row| row =~ regex }
514
+ when Hash
515
+ exception_data[section] && exception_data[section][:to_s] =~ regex
516
+ when NilClass
517
+ false
518
+ else
519
+ raise "Unexpected class #{exception_data[section].class.name}"
520
+ end
521
+ end
522
+ end
523
+ end
524
+
525
+ def initialize( filter_path )
526
+ @filter_path = filter_path
527
+ @filters = { }
528
+ @filters_last_modified_time = nil
529
+ end
530
+
531
+ def filtered?( exception_data )
532
+ refresh_filters
533
+
534
+ @filters.any? do |name, filter|
535
+ if ( match = filter.match?( exception_data ) )
536
+ ExceptionHandling.logger.warn( "Filtered exception using '#{name}'; not sending email to notify" )
537
+ end
538
+ match
539
+ end
540
+ end
541
+
542
+ private
543
+
544
+ def refresh_filters
545
+ mtime = last_modified_time
546
+ if @filters_last_modified_time.nil? || mtime != @filters_last_modified_time
547
+ ExceptionHandling.logger.info( "Reloading filter list from: #{@filter_path}. Last loaded time: #{@filters_last_modified_time}. Last modified time: #{mtime}" )
548
+ @filters_last_modified_time = mtime # make race condition fall on the side of reloading unnecessarily next time rather than missing a set of changes
549
+
550
+ @filters = load_file
551
+ end
552
+
553
+ rescue => ex # any exceptions
554
+ ExceptionHandling::log_error( ex, "ExceptionRegexes::refresh_filters: #{@filter_path}", nil, true)
555
+ end
556
+
557
+ def load_file
558
+ # store all strings from YAML file into regex's on initial load, instead of converting to regex on every exception that is logged
559
+ filters = YAML::load_file( @filter_path )
560
+ Hash[ filters.map do |filter_name, regexes|
561
+ [filter_name, Filter.new( filter_name, regexes )]
562
+ end ]
563
+ end
564
+
565
+ def last_modified_time
566
+ File.mtime( @filter_path )
567
+ end
568
+ end
569
+
570
+
571
+ public
572
+
573
+ module Methods # included on models and controllers
574
+ protected
575
+ def log_error(exception_or_string, exception_context = '')
576
+ controller = self if respond_to?(:request) && respond_to?(:session)
577
+ ExceptionHandling.log_error(exception_or_string, exception_context, controller)
578
+ end
579
+
580
+ def log_error_rack(exception_or_string, exception_context = '', rack_filter = '')
581
+ ExceptionHandling.log_error_rack(exception_or_string, exception_context, controller)
582
+ end
583
+
584
+ def log_warning(message)
585
+ log_error(Warning.new(message))
586
+ end
587
+
588
+ def log_info(message)
589
+ ExceptionHandling.logger.info( message )
590
+ end
591
+
592
+ def log_debug(message)
593
+ ExceptionHandling.logger.debug( message )
594
+ end
595
+
596
+ def ensure_safe(exception_context = "")
597
+ begin
598
+ yield
599
+ rescue => ex
600
+ log_error ex, exception_context
601
+ nil
602
+ end
603
+ end
604
+
605
+ def ensure_escalation(*args)
606
+ ExceptionHandling.ensure_escalation(*args) do
607
+ yield
608
+ end
609
+ end
610
+
611
+ # Store aside the current controller when included
612
+ LONG_REQUEST_SECONDS = (defined?(Rails) && Rails.env == 'test' ? 300 : 30)
613
+ def set_current_controller
614
+ ExceptionHandling.current_controller = self
615
+ result = nil
616
+ time = Benchmark.measure do
617
+ result = yield
618
+ end
619
+ name = " in #{controller_name}::#{action_name}" rescue " "
620
+ log_error( "Long controller action detected#{name} %.4fs " % time.real ) if time.real > LONG_REQUEST_SECONDS && !['development', 'test'].include?(Rails.env)
621
+ result
622
+ ensure
623
+ ExceptionHandling.current_controller = nil
624
+ end
625
+
626
+ def self.included( controller )
627
+ controller.around_filter :set_current_controller if controller.respond_to? :around_filter
628
+ end
629
+ end
630
+ end