exception_handling 2.2.1 → 2.3.0.pre.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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.rubocop.yml +5 -0
- data/Gemfile +7 -6
- data/Gemfile.lock +26 -23
- data/README.md +0 -1
- data/Rakefile +4 -4
- data/config/exception_filters.yml +2 -3
- data/exception_handling.gemspec +12 -10
- data/lib/exception_handling/exception_catalog.rb +5 -4
- data/lib/exception_handling/exception_description.rb +28 -28
- data/lib/exception_handling/exception_info.rb +80 -72
- data/lib/exception_handling/honeybadger_callbacks.rb +41 -24
- data/lib/exception_handling/log_stub_error.rb +10 -8
- data/lib/exception_handling/mailer.rb +27 -48
- data/lib/exception_handling/methods.rb +15 -11
- data/lib/exception_handling/sensu.rb +7 -5
- data/lib/exception_handling/testing.rb +21 -19
- data/lib/exception_handling/version.rb +1 -1
- data/lib/exception_handling.rb +105 -200
- data/test/helpers/controller_helpers.rb +3 -1
- data/test/helpers/exception_helpers.rb +9 -0
- data/test/test_helper.rb +26 -21
- data/test/unit/exception_handling/exception_catalog_test.rb +15 -14
- data/test/unit/exception_handling/exception_description_test.rb +22 -31
- data/test/unit/exception_handling/exception_info_test.rb +76 -37
- data/test/unit/exception_handling/honeybadger_callbacks_test.rb +46 -9
- data/test/unit/exception_handling/log_error_stub_test.rb +6 -4
- data/test/unit/exception_handling/mailer_test.rb +8 -14
- data/test/unit/exception_handling/methods_test.rb +9 -6
- data/test/unit/exception_handling/sensu_test.rb +6 -4
- data/test/unit/exception_handling_test.rb +279 -364
- metadata +24 -24
- data/views/exception_handling/mailer/exception_notification.html.erb +0 -92
data/lib/exception_handling.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'timeout'
|
2
4
|
require 'active_support'
|
3
5
|
require 'active_support/core_ext/hash'
|
@@ -17,13 +19,12 @@ require "exception_handling/honeybadger_callbacks.rb"
|
|
17
19
|
_ = ActiveSupport::HashWithIndifferentAccess
|
18
20
|
|
19
21
|
module ExceptionHandling # never included
|
20
|
-
|
21
22
|
class Warning < StandardError; end
|
22
23
|
class MailerTimeout < Timeout::Error; end
|
23
24
|
class ClientLoggingError < StandardError; end
|
24
25
|
|
25
26
|
SUMMARY_THRESHOLD = 5
|
26
|
-
SUMMARY_PERIOD = 60*60 # 1.hour
|
27
|
+
SUMMARY_PERIOD = 60 * 60 # 1.hour
|
27
28
|
|
28
29
|
AUTHENTICATION_HEADERS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION', 'REDIRECT_X_HTTP_AUTHORIZATION'].freeze
|
29
30
|
HONEYBADGER_STATUSES = [:success, :failure, :skipped].freeze
|
@@ -38,19 +39,19 @@ module ExceptionHandling # never included
|
|
38
39
|
attr_writer :exception_recipients
|
39
40
|
|
40
41
|
def server_name
|
41
|
-
@server_name or raise ArgumentError, "You must assign a value to #{
|
42
|
+
@server_name or raise ArgumentError, "You must assign a value to #{name}.server_name"
|
42
43
|
end
|
43
44
|
|
44
45
|
def sender_address
|
45
|
-
@sender_address or raise ArgumentError, "You must assign a value to #{
|
46
|
+
@sender_address or raise ArgumentError, "You must assign a value to #{name}.sender_address"
|
46
47
|
end
|
47
48
|
|
48
49
|
def exception_recipients
|
49
|
-
@exception_recipients or raise ArgumentError, "You must assign a value to #{
|
50
|
+
@exception_recipients or raise ArgumentError, "You must assign a value to #{name}.exception_recipients"
|
50
51
|
end
|
51
52
|
|
52
53
|
def logger
|
53
|
-
@logger or raise ArgumentError, "You must assign a value to #{
|
54
|
+
@logger or raise ArgumentError, "You must assign a value to #{name}.logger"
|
54
55
|
end
|
55
56
|
|
56
57
|
def logger=(logger)
|
@@ -87,7 +88,6 @@ module ExceptionHandling # never included
|
|
87
88
|
attr_accessor :production_support_recipients
|
88
89
|
attr_accessor :escalation_recipients
|
89
90
|
attr_accessor :email_environment
|
90
|
-
attr_accessor :mailer_send_enabled
|
91
91
|
attr_accessor :custom_data_hook
|
92
92
|
attr_accessor :post_log_error_hook
|
93
93
|
attr_accessor :stub_handler
|
@@ -100,7 +100,6 @@ module ExceptionHandling # never included
|
|
100
100
|
attr_reader :eventmachine_synchrony
|
101
101
|
|
102
102
|
@filter_list_filename = "./config/exception_filters.yml"
|
103
|
-
@mailer_send_enabled = true
|
104
103
|
@email_environment = ""
|
105
104
|
@eventmachine_safe = false
|
106
105
|
@eventmachine_synchrony = false
|
@@ -111,8 +110,9 @@ module ExceptionHandling # never included
|
|
111
110
|
# set this for operation within an eventmachine reactor
|
112
111
|
def eventmachine_safe=(bool)
|
113
112
|
if bool != true && bool != false
|
114
|
-
raise ArgumentError, "#{
|
113
|
+
raise ArgumentError, "#{name}.eventmachine_safe must be a boolean."
|
115
114
|
end
|
115
|
+
|
116
116
|
if bool
|
117
117
|
require 'eventmachine'
|
118
118
|
require 'em/protocols/smtpclient'
|
@@ -123,8 +123,9 @@ module ExceptionHandling # never included
|
|
123
123
|
# set this for EM::Synchrony async operation
|
124
124
|
def eventmachine_synchrony=(bool)
|
125
125
|
if bool != true && bool != false
|
126
|
-
raise ArgumentError, "#{
|
126
|
+
raise ArgumentError, "#{name}.eventmachine_synchrony must be a boolean."
|
127
127
|
end
|
128
|
+
|
128
129
|
@eventmachine_synchrony = bool
|
129
130
|
end
|
130
131
|
|
@@ -148,67 +149,60 @@ module ExceptionHandling # never included
|
|
148
149
|
# Gets called by Rack Middleware: DebugExceptions or ShowExceptions
|
149
150
|
# it does 2 things:
|
150
151
|
# log the error
|
151
|
-
#
|
152
|
+
# may send to honeybadger
|
152
153
|
#
|
153
154
|
# but not during functional tests, when rack middleware is not used
|
154
155
|
#
|
155
|
-
def log_error_rack(exception, env,
|
156
|
+
def log_error_rack(exception, env, _rack_filter)
|
156
157
|
timestamp = set_log_error_timestamp
|
157
158
|
exception_info = ExceptionInfo.new(exception, env, timestamp)
|
158
159
|
|
159
160
|
if stub_handler
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
write_exception_to_log(exception, custom_description, timestamp)
|
161
|
+
stub_handler.handle_stub_log_error(exception_info.data)
|
162
|
+
else
|
163
|
+
# TODO: add a more interesting custom description, like:
|
164
|
+
# custom_description = ": caught and processed by Rack middleware filter #{rack_filter}"
|
165
|
+
# which would be nice, but would also require changing quite a few tests
|
166
|
+
custom_description = ""
|
167
|
+
write_exception_to_log(exception, custom_description, timestamp)
|
168
168
|
|
169
|
-
|
170
|
-
send_exception_to_honeybadger(exception_info)
|
171
|
-
end
|
169
|
+
send_external_notifications(exception_info)
|
172
170
|
|
173
|
-
|
174
|
-
# controller may not exist in some cases (like most 404 errors)
|
175
|
-
if (controller = exception_info.controller)
|
176
|
-
controller.session["last_exception_timestamp"] = last_exception_timestamp
|
177
|
-
end
|
178
|
-
log_error_email(exception_info)
|
171
|
+
nil
|
179
172
|
end
|
180
173
|
end
|
181
174
|
|
182
175
|
#
|
183
176
|
# Normal Operation:
|
184
177
|
# Called directly by our code, usually from rescue blocks.
|
185
|
-
#
|
178
|
+
# Writes to log file and may send to honeybadger
|
186
179
|
#
|
187
180
|
# Functional Test Operation:
|
188
|
-
# Calls into handle_stub_log_error and returns. no log file. no
|
181
|
+
# Calls into handle_stub_log_error and returns. no log file. no honeybadger
|
189
182
|
#
|
190
183
|
def log_error(exception_or_string, exception_context = '', controller = nil, treat_like_warning: false, **log_context, &data_callback)
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
execute_custom_log_error_callback(exception_info.enhanced_data, exception_info.exception, treat_like_warning, external_notification_results)
|
204
|
-
nil
|
205
|
-
end
|
206
|
-
rescue LogErrorStub::UnexpectedExceptionLogged, LogErrorStub::ExpectedExceptionNotLogged
|
207
|
-
raise
|
208
|
-
rescue Exception => ex
|
209
|
-
$stderr.puts("ExceptionHandlingError: log_error rescued exception while logging #{exception_context}: #{exception_or_string}:\n#{ex.class}: #{ex.message}\n#{ex.backtrace.join("\n")}")
|
210
|
-
write_exception_to_log(ex, "ExceptionHandlingError: log_error rescued exception while logging #{exception_context}: #{exception_or_string}", timestamp)
|
184
|
+
ex = make_exception(exception_or_string)
|
185
|
+
timestamp = set_log_error_timestamp
|
186
|
+
exception_info = ExceptionInfo.new(ex, exception_context, timestamp, controller || current_controller, data_callback)
|
187
|
+
|
188
|
+
if stub_handler
|
189
|
+
stub_handler.handle_stub_log_error(exception_info.data)
|
190
|
+
else
|
191
|
+
write_exception_to_log(ex, exception_context, timestamp, **log_context)
|
192
|
+
external_notification_results = unless treat_like_warning || ex.is_a?(Warning)
|
193
|
+
send_external_notifications(exception_info)
|
194
|
+
end || {}
|
195
|
+
execute_custom_log_error_callback(exception_info.enhanced_data.merge(log_context: log_context), exception_info.exception, treat_like_warning, external_notification_results)
|
211
196
|
end
|
197
|
+
|
198
|
+
ExceptionHandling.last_exception_timestamp
|
199
|
+
rescue LogErrorStub::UnexpectedExceptionLogged, LogErrorStub::ExpectedExceptionNotLogged
|
200
|
+
raise
|
201
|
+
rescue Exception => ex
|
202
|
+
warn("ExceptionHandlingError: log_error rescued exception while logging #{exception_context}: #{exception_or_string}:\n#{ex.class}: #{ex.message}\n#{ex.backtrace.join("\n")}")
|
203
|
+
write_exception_to_log(ex, "ExceptionHandlingError: log_error rescued exception while logging #{exception_context}: #{exception_or_string}", timestamp)
|
204
|
+
ensure
|
205
|
+
ExceptionHandling.last_exception_timestamp
|
212
206
|
end
|
213
207
|
|
214
208
|
#
|
@@ -225,34 +219,38 @@ module ExceptionHandling # never included
|
|
225
219
|
#
|
226
220
|
def send_external_notifications(exception_info)
|
227
221
|
results = {}
|
228
|
-
if
|
229
|
-
results[:honeybadger_status] =
|
222
|
+
if honeybadger_defined?
|
223
|
+
results[:honeybadger_status] = send_exception_to_honeybadger_unless_filtered(exception_info)
|
230
224
|
end
|
225
|
+
results
|
226
|
+
end
|
231
227
|
|
232
|
-
|
233
|
-
|
228
|
+
# Returns :success or :failure or :skipped
|
229
|
+
def send_exception_to_honeybadger_unless_filtered(exception_info)
|
230
|
+
if exception_info.send_to_honeybadger?
|
231
|
+
send_exception_to_honeybadger(exception_info)
|
232
|
+
else
|
233
|
+
log_info("Filtered exception using '#{exception_info.exception_description.filter_name}'; not sending notification to Honeybadger")
|
234
|
+
:skipped
|
234
235
|
end
|
235
|
-
results
|
236
236
|
end
|
237
237
|
|
238
238
|
#
|
239
239
|
# Log exception to honeybadger.io.
|
240
240
|
#
|
241
|
+
# Returns :success or :failure
|
242
|
+
#
|
241
243
|
def send_exception_to_honeybadger(exception_info)
|
242
244
|
exception = exception_info.exception
|
243
245
|
exception_description = exception_info.exception_description
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
else
|
251
|
-
log_info("Filtered exception using '#{exception_description.filter_name}'; not sending notification to Honeybadger")
|
252
|
-
:skipped
|
253
|
-
end
|
246
|
+
response = Honeybadger.notify(error_class: exception_description ? exception_description.filter_name : exception.class.name,
|
247
|
+
error_message: exception.message.to_s,
|
248
|
+
exception: exception,
|
249
|
+
context: exception_info.honeybadger_context_data,
|
250
|
+
controller: exception_info.controller_name)
|
251
|
+
response ? :success : :failure
|
254
252
|
rescue Exception => ex
|
255
|
-
|
253
|
+
warn("ExceptionHandling.send_exception_to_honeybadger rescued exception while logging #{exception_info.exception_context}:\n#{exception.class}: #{exception.message}:\n#{ex.class}: #{ex.message}\n#{ex.backtrace.join("\n")}")
|
256
254
|
write_exception_to_log(ex, "ExceptionHandling.send_exception_to_honeybadger rescued exception while logging #{exception_info.exception_context}:\n#{exception.class}: #{exception.message}", exception_info.timestamp)
|
257
255
|
:failure
|
258
256
|
end
|
@@ -260,7 +258,7 @@ module ExceptionHandling # never included
|
|
260
258
|
#
|
261
259
|
# Check if Honeybadger defined.
|
262
260
|
#
|
263
|
-
def
|
261
|
+
def honeybadger_defined?
|
264
262
|
Object.const_defined?("Honeybadger")
|
265
263
|
end
|
266
264
|
|
@@ -295,7 +293,7 @@ module ExceptionHandling # never included
|
|
295
293
|
yield
|
296
294
|
rescue => ex
|
297
295
|
log_error ex, exception_context, **log_context
|
298
|
-
|
296
|
+
nil
|
299
297
|
end
|
300
298
|
|
301
299
|
def ensure_completely_safe(exception_context = "", **log_context)
|
@@ -304,33 +302,32 @@ module ExceptionHandling # never included
|
|
304
302
|
raise
|
305
303
|
rescue Exception => ex
|
306
304
|
log_error ex, exception_context, log_context
|
305
|
+
nil
|
307
306
|
end
|
308
307
|
|
309
308
|
def escalate_to_production_support(exception_or_string, email_subject)
|
310
|
-
production_support_recipients or raise ArgumentError, "In order to escalate to production support, you must set #{
|
309
|
+
production_support_recipients or raise ArgumentError, "In order to escalate to production support, you must set #{name}.production_recipients"
|
311
310
|
ex = make_exception(exception_or_string)
|
312
|
-
|
311
|
+
escalate(email_subject, ex, last_exception_timestamp, production_support_recipients)
|
313
312
|
end
|
314
313
|
|
315
|
-
def escalate_error(exception_or_string, email_subject, **log_context)
|
314
|
+
def escalate_error(exception_or_string, email_subject, custom_recipients = nil, **log_context)
|
316
315
|
ex = make_exception(exception_or_string)
|
317
316
|
log_error(ex, **log_context)
|
318
|
-
escalate(email_subject, ex, last_exception_timestamp)
|
317
|
+
escalate(email_subject, ex, last_exception_timestamp, custom_recipients)
|
319
318
|
end
|
320
319
|
|
321
|
-
def escalate_warning(message, email_subject, **log_context)
|
320
|
+
def escalate_warning(message, email_subject, custom_recipients = nil, **log_context)
|
322
321
|
ex = Warning.new(message)
|
323
322
|
log_error(ex, **log_context)
|
324
|
-
escalate(email_subject, ex, last_exception_timestamp)
|
323
|
+
escalate(email_subject, ex, last_exception_timestamp, custom_recipients)
|
325
324
|
end
|
326
325
|
|
327
|
-
def ensure_escalation(email_subject, **log_context)
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
nil
|
333
|
-
end
|
326
|
+
def ensure_escalation(email_subject, custom_recipients = nil, **log_context)
|
327
|
+
yield
|
328
|
+
rescue => ex
|
329
|
+
escalate_error(ex, email_subject, custom_recipients, **log_context)
|
330
|
+
nil
|
334
331
|
end
|
335
332
|
|
336
333
|
def alert_warning(exception_or_string, alert_name, exception_context, **log_context)
|
@@ -338,28 +335,22 @@ module ExceptionHandling # never included
|
|
338
335
|
log_error(ex, exception_context, **log_context)
|
339
336
|
begin
|
340
337
|
ExceptionHandling::Sensu.generate_event(alert_name, exception_context.to_s + "\n" + encode_utf8(ex.message.to_s))
|
341
|
-
rescue =>
|
342
|
-
log_error(
|
338
|
+
rescue => ex
|
339
|
+
log_error(ex, 'ExceptionHandling.alert_warning')
|
343
340
|
end
|
344
341
|
end
|
345
342
|
|
346
343
|
def ensure_alert(alert_name, exception_context, **log_context)
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
nil
|
352
|
-
end
|
344
|
+
yield
|
345
|
+
rescue => ex
|
346
|
+
alert_warning(ex, alert_name, exception_context, **log_context)
|
347
|
+
nil
|
353
348
|
end
|
354
349
|
|
355
350
|
def set_log_error_timestamp
|
356
351
|
ExceptionHandling.last_exception_timestamp = Time.now.to_i
|
357
352
|
end
|
358
353
|
|
359
|
-
def should_send_email?
|
360
|
-
ExceptionHandling.mailer_send_enabled
|
361
|
-
end
|
362
|
-
|
363
354
|
def trace_timing(description)
|
364
355
|
result = nil
|
365
356
|
time = Benchmark.measure do
|
@@ -381,19 +372,19 @@ module ExceptionHandling # never included
|
|
381
372
|
def encode_utf8(string)
|
382
373
|
string.encode('UTF-8',
|
383
374
|
replace: '?',
|
384
|
-
undef:
|
375
|
+
undef: :replace,
|
385
376
|
invalid: :replace)
|
386
377
|
end
|
387
378
|
|
388
379
|
def clean_backtrace(exception)
|
389
380
|
backtrace = if exception.backtrace.nil?
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
381
|
+
['<no backtrace>']
|
382
|
+
elsif exception.is_a?(ClientLoggingError)
|
383
|
+
exception.backtrace
|
384
|
+
elsif defined?(Rails) && defined?(Rails.backtrace_cleaner)
|
385
|
+
Rails.backtrace_cleaner.clean(exception.backtrace)
|
386
|
+
else
|
387
|
+
exception.backtrace
|
397
388
|
end
|
398
389
|
|
399
390
|
# The rails backtrace cleaner returns an empty array for a backtrace if the exception was raised outside the app (inside a gem for instance)
|
@@ -406,25 +397,6 @@ module ExceptionHandling # never included
|
|
406
397
|
|
407
398
|
private
|
408
399
|
|
409
|
-
def log_error_email(exception_info)
|
410
|
-
data = exception_info.enhanced_data
|
411
|
-
exception_description = exception_info.exception_description
|
412
|
-
|
413
|
-
if exception_description && !exception_description.send_email
|
414
|
-
ExceptionHandling.logger.warn(
|
415
|
-
"Filtered exception using '#{exception_description.filter_name}'; not sending email to notify"
|
416
|
-
)
|
417
|
-
else
|
418
|
-
if summarize_exception(data) != :Summarized
|
419
|
-
deliver(ExceptionHandling::Mailer.exception_notification(data))
|
420
|
-
end
|
421
|
-
end
|
422
|
-
nil
|
423
|
-
rescue Exception => ex
|
424
|
-
$stderr.puts("ExceptionHandling.log_error_email rescued exception while logging #{exception_info.exception_context}: #{exception_info.exception}:\n#{ex.class}: #{ex}\n#{ex.backtrace.join("\n")}")
|
425
|
-
write_exception_to_log(ex, "ExceptionHandling.log_error_email rescued exception while logging #{exception_info.exception_context}: #{exception_info.exception}", exception_info.timestamp)
|
426
|
-
end
|
427
|
-
|
428
400
|
def execute_custom_log_error_callback(exception_data, exception, treat_like_warning, external_notification_results)
|
429
401
|
if ExceptionHandling.post_log_error_hook
|
430
402
|
honeybadger_status = external_notification_results[:honeybadger_status] || :skipped
|
@@ -437,14 +409,9 @@ module ExceptionHandling # never included
|
|
437
409
|
log_info("Unable to execute custom log_error callback. #{ex_message} #{ex_backtrace}")
|
438
410
|
end
|
439
411
|
|
440
|
-
def escalate(email_subject, ex, timestamp)
|
412
|
+
def escalate(email_subject, ex, timestamp, custom_recipients = nil)
|
441
413
|
exception_info = ExceptionInfo.new(ex, nil, timestamp)
|
442
|
-
deliver(ExceptionHandling::Mailer.escalation_notification(email_subject, exception_info.data))
|
443
|
-
end
|
444
|
-
|
445
|
-
def escalate_custom(email_subject, ex, timestamp, recipients)
|
446
|
-
exception_info = ExceptionInfo.new(ex, nil, timestamp)
|
447
|
-
deliver(ExceptionHandling::Mailer.escalate_custom(email_subject, exception_info.data, recipients))
|
414
|
+
deliver(ExceptionHandling::Mailer.escalation_notification(email_subject, exception_info.data, custom_recipients))
|
448
415
|
end
|
449
416
|
|
450
417
|
def deliver(mail_object)
|
@@ -455,20 +422,18 @@ module ExceptionHandling # never included
|
|
455
422
|
dns_deferrable = EventMachine::DNS::Resolver.resolve(smtp_settings[:address])
|
456
423
|
dns_deferrable.callback do |addrs|
|
457
424
|
send_deferrable = EventMachine::Protocols::SmtpClient.__send__(
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
:content => "#{mail_object}\r\n.\r\n"
|
467
|
-
}
|
425
|
+
async_send_method,
|
426
|
+
host: addrs.first,
|
427
|
+
port: smtp_settings[:port],
|
428
|
+
domain: smtp_settings[:domain],
|
429
|
+
auth: { type: :plain, username: smtp_settings[:user_name], password: smtp_settings[:password] },
|
430
|
+
from: mail_object['from'].to_s,
|
431
|
+
to: mail_object['to'].to_s,
|
432
|
+
content: "#{mail_object}\r\n.\r\n"
|
468
433
|
)
|
469
434
|
send_deferrable.errback { |err| ExceptionHandling.logger.fatal("Failed to email by SMTP: #{err.inspect}") }
|
470
435
|
end
|
471
|
-
dns_deferrable.errback
|
436
|
+
dns_deferrable.errback { |err| ExceptionHandling.logger.fatal("Failed to resolv DNS for #{smtp_settings[:address]}: #{err.inspect}") }
|
472
437
|
end
|
473
438
|
else
|
474
439
|
safe_email_deliver do
|
@@ -478,73 +443,13 @@ module ExceptionHandling # never included
|
|
478
443
|
end
|
479
444
|
|
480
445
|
def safe_email_deliver
|
481
|
-
Timeout
|
446
|
+
Timeout.timeout 30, MailerTimeout do
|
482
447
|
yield
|
483
448
|
end
|
484
449
|
rescue StandardError, MailerTimeout => ex
|
485
|
-
#$stderr.puts("ExceptionHandling::safe_email_deliver rescued: #{ex.class}: #{ex}\n#{ex.backtrace.join("\n")}")
|
486
450
|
log_error(ex, "ExceptionHandling::safe_email_deliver", nil, treat_like_warning: true)
|
487
451
|
end
|
488
452
|
|
489
|
-
def clear_exception_summary
|
490
|
-
@last_exception = nil
|
491
|
-
end
|
492
|
-
|
493
|
-
# Returns :Summarized iff exception has been added to summary and therefore should not be sent.
|
494
|
-
def summarize_exception(data)
|
495
|
-
if defined?(@last_exception) && @last_exception
|
496
|
-
same_signature = @last_exception[:backtrace] == data[:backtrace]
|
497
|
-
|
498
|
-
case @last_exception[:state]
|
499
|
-
|
500
|
-
when :NotSummarized
|
501
|
-
if same_signature
|
502
|
-
@last_exception[:count] += 1
|
503
|
-
if @last_exception[:count] >= SUMMARY_THRESHOLD
|
504
|
-
@last_exception.merge! :state => :Summarized, :first_seen => Time.now, :count => 0
|
505
|
-
end
|
506
|
-
return nil
|
507
|
-
end
|
508
|
-
|
509
|
-
when :Summarized
|
510
|
-
if same_signature
|
511
|
-
@last_exception[:count] += 1
|
512
|
-
if Time.now - @last_exception[:first_seen] > SUMMARY_PERIOD
|
513
|
-
send_exception_summary(data, @last_exception[:first_seen], @last_exception[:count])
|
514
|
-
@last_exception.merge! :first_seen => Time.now, :count => 0
|
515
|
-
end
|
516
|
-
return :Summarized
|
517
|
-
elsif @last_exception[:count] > 0 # send the left-over, if any
|
518
|
-
send_exception_summary(@last_exception[:data], @last_exception[:first_seen], @last_exception[:count])
|
519
|
-
end
|
520
|
-
|
521
|
-
else
|
522
|
-
raise "Unknown state #{@last_exception[:state]}"
|
523
|
-
end
|
524
|
-
end
|
525
|
-
|
526
|
-
# New signature we haven't seen before. Not summarized yet--we're just starting the count.
|
527
|
-
@last_exception = {
|
528
|
-
:data => data,
|
529
|
-
:count => 1,
|
530
|
-
:first_seen => Time.now,
|
531
|
-
:backtrace => data[:backtrace],
|
532
|
-
:state => :NotSummarized
|
533
|
-
}
|
534
|
-
nil
|
535
|
-
end
|
536
|
-
|
537
|
-
def send_exception_summary(exception_data, first_seen, occurrences)
|
538
|
-
Timeout::timeout 30, MailerTimeout do
|
539
|
-
deliver(ExceptionHandling::Mailer.exception_notification(exception_data, first_seen, occurrences))
|
540
|
-
end
|
541
|
-
rescue StandardError, MailerTimeout => ex
|
542
|
-
original_error = exception_data[:error_string]
|
543
|
-
log_prefix = "ExceptionHandling.log_error_email rescued exception while logging #{original_error}"
|
544
|
-
$stderr.puts("#{log_prefix}:\n#{ex.class}: #{ex}\n#{ex.backtrace.join("\n")}")
|
545
|
-
log_info(log_prefix)
|
546
|
-
end
|
547
|
-
|
548
453
|
def make_exception(exception_or_string)
|
549
454
|
if exception_or_string.is_a?(Exception)
|
550
455
|
exception_or_string
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ExceptionHelpers
|
2
4
|
def raise_exception_with_nil_message
|
3
5
|
raise exception_with_nil_message
|
@@ -8,4 +10,11 @@ module ExceptionHelpers
|
|
8
10
|
stub(exception_with_nil_message).message { nil }
|
9
11
|
exception_with_nil_message
|
10
12
|
end
|
13
|
+
|
14
|
+
attr_reader :sent_notifications
|
15
|
+
|
16
|
+
def capture_notifications
|
17
|
+
@sent_notifications = []
|
18
|
+
stub(ExceptionHandling).send_exception_to_honeybadger(anything) { |exception_info| @sent_notifications << exception_info }
|
19
|
+
end
|
11
20
|
end
|
data/test/test_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support'
|
2
4
|
require 'active_support/time'
|
3
5
|
require 'active_support/test_case'
|
@@ -67,11 +69,11 @@ class SocketStub
|
|
67
69
|
end
|
68
70
|
end
|
69
71
|
|
70
|
-
|
72
|
+
ExceptionHandling.logger = LoggerStub.new
|
71
73
|
|
72
|
-
|
73
|
-
|
74
|
-
|
74
|
+
def dont_stub_log_error
|
75
|
+
true
|
76
|
+
end
|
75
77
|
|
76
78
|
ActionMailer::Base.delivery_method = :test
|
77
79
|
|
@@ -88,12 +90,12 @@ class ActiveSupport::TestCase
|
|
88
90
|
|
89
91
|
unless defined?(Rails) && defined?(Rails.env)
|
90
92
|
module ::Rails
|
91
|
-
|
92
|
-
|
93
|
-
end
|
93
|
+
class << self
|
94
|
+
attr_writer :env
|
94
95
|
|
95
|
-
|
96
|
-
|
96
|
+
def env
|
97
|
+
@env ||= 'test'
|
98
|
+
end
|
97
99
|
end
|
98
100
|
end
|
99
101
|
end
|
@@ -107,7 +109,6 @@ class ActiveSupport::TestCase
|
|
107
109
|
ExceptionHandling.exception_recipients = 'exceptions@example.com'
|
108
110
|
ExceptionHandling.escalation_recipients = 'escalation@example.com'
|
109
111
|
ExceptionHandling.server_name = 'server'
|
110
|
-
ExceptionHandling.mailer_send_enabled = true
|
111
112
|
ExceptionHandling.filter_list_filename = "./config/exception_filters.yml"
|
112
113
|
ExceptionHandling.eventmachine_safe = false
|
113
114
|
ExceptionHandling.eventmachine_synchrony = false
|
@@ -117,7 +118,7 @@ class ActiveSupport::TestCase
|
|
117
118
|
end
|
118
119
|
|
119
120
|
teardown do
|
120
|
-
@@constant_overrides
|
121
|
+
@@constant_overrides&.reverse&.each do |parent_module, k, v|
|
121
122
|
ExceptionHandling.ensure_safe "constant cleanup #{k.inspect}, #{parent_module}(#{parent_module.class})::#{v.inspect}(#{v.class})" do
|
122
123
|
silence_warnings do
|
123
124
|
if v == :never_defined
|
@@ -141,7 +142,11 @@ class ActiveSupport::TestCase
|
|
141
142
|
parent_module == :never_defined and raise "You need to set each parent constant earlier! #{nested_const_name}"
|
142
143
|
final_parent_module = parent_module
|
143
144
|
final_const_name = nested_const_name
|
144
|
-
|
145
|
+
begin
|
146
|
+
parent_module.const_get(nested_const_name)
|
147
|
+
rescue
|
148
|
+
:never_defined
|
149
|
+
end
|
145
150
|
end
|
146
151
|
|
147
152
|
@@constant_overrides << [final_parent_module, final_const_name, original_value]
|
@@ -156,11 +161,11 @@ class ActiveSupport::TestCase
|
|
156
161
|
else
|
157
162
|
original_count = 0
|
158
163
|
end
|
159
|
-
assert_equal expected, ActionMailer::Base.deliveries.size - original_count, "wrong number of emails#{
|
164
|
+
assert_equal expected, ActionMailer::Base.deliveries.size - original_count, "wrong number of emails#{': ' + message.to_s if message}"
|
160
165
|
end
|
161
166
|
end
|
162
167
|
|
163
|
-
def assert_equal_with_diff
|
168
|
+
def assert_equal_with_diff(arg1, arg2, msg = '')
|
164
169
|
if arg1 == arg2
|
165
170
|
assert true # To keep the assertion count accurate
|
166
171
|
else
|
@@ -176,22 +181,22 @@ class Time
|
|
176
181
|
class << self
|
177
182
|
attr_reader :now_override
|
178
183
|
|
179
|
-
def now_override=
|
180
|
-
if ActiveSupport::TimeWithZone
|
184
|
+
def now_override=(override_time)
|
185
|
+
if override_time.is_a?(ActiveSupport::TimeWithZone)
|
181
186
|
override_time = override_time.localtime
|
182
187
|
else
|
183
|
-
override_time.nil? || Time
|
188
|
+
override_time.nil? || override_time.is_a?(Time) or raise "override_time should be a Time object, but was a #{override_time.class.name}"
|
184
189
|
end
|
185
190
|
@now_override = override_time
|
186
191
|
end
|
187
192
|
|
188
|
-
unless defined?
|
193
|
+
unless defined?(@@_old_now_defined)
|
189
194
|
alias old_now now
|
190
195
|
@@_old_now_defined = true
|
191
196
|
end
|
192
|
-
end
|
193
197
|
|
194
|
-
|
195
|
-
|
198
|
+
def now
|
199
|
+
now_override ? now_override.dup : old_now
|
200
|
+
end
|
196
201
|
end
|
197
202
|
end
|