exception_handling 2.5.0 → 2.7.0
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 +4 -4
- data/.gitignore +1 -1
- data/.jenkins/Jenkinsfile +24 -8
- data/.rspec +3 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +4 -4
- data/Gemfile.lock +67 -58
- data/Rakefile +7 -6
- data/gemfiles/rails_4.gemfile +4 -4
- data/gemfiles/rails_5.gemfile +4 -4
- data/gemfiles/rails_6.gemfile +4 -4
- data/lib/exception_handling.rb +9 -3
- data/lib/exception_handling/exception_info.rb +3 -6
- data/lib/exception_handling/log_stub_error.rb +2 -1
- data/lib/exception_handling/logging_methods.rb +27 -0
- data/lib/exception_handling/methods.rb +6 -53
- data/lib/exception_handling/testing.rb +20 -10
- data/lib/exception_handling/version.rb +1 -1
- data/{test → spec}/helpers/controller_helpers.rb +0 -0
- data/{test → spec}/helpers/exception_helpers.rb +2 -2
- data/{test → spec}/rake_test_warning_false.rb +0 -0
- data/{test/test_helper.rb → spec/spec_helper.rb} +57 -42
- data/spec/unit/exception_handling/exception_catalog_spec.rb +85 -0
- data/spec/unit/exception_handling/exception_description_spec.rb +82 -0
- data/{test/unit/exception_handling/exception_info_test.rb → spec/unit/exception_handling/exception_info_spec.rb} +118 -99
- data/{test/unit/exception_handling/honeybadger_callbacks_test.rb → spec/unit/exception_handling/honeybadger_callbacks_spec.rb} +20 -20
- data/{test/unit/exception_handling/log_error_stub_test.rb → spec/unit/exception_handling/log_error_stub_spec.rb} +38 -22
- data/spec/unit/exception_handling/logging_methods_spec.rb +38 -0
- data/{test/unit/exception_handling/mailer_test.rb → spec/unit/exception_handling/mailer_spec.rb} +17 -17
- data/spec/unit/exception_handling/methods_spec.rb +105 -0
- data/spec/unit/exception_handling/sensu_spec.rb +51 -0
- data/{test/unit/exception_handling_test.rb → spec/unit/exception_handling_spec.rb} +349 -319
- metadata +35 -31
- data/test/unit/exception_handling/exception_catalog_test.rb +0 -85
- data/test/unit/exception_handling/exception_description_test.rb +0 -82
- data/test/unit/exception_handling/methods_test.rb +0 -84
- data/test/unit/exception_handling/sensu_test.rb +0 -52
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('../../spec_helper', __dir__)
|
4
|
+
|
5
|
+
module ExceptionHandling
|
6
|
+
describe Sensu do
|
7
|
+
context "#generate_event" do
|
8
|
+
it "create an event" do
|
9
|
+
expect(ExceptionHandling::Sensu).to receive(:send_event).with(name: "world_is_ending", output: "stick head between knees and kiss ass goodbye", status: 1)
|
10
|
+
|
11
|
+
ExceptionHandling::Sensu.generate_event("world_is_ending", "stick head between knees and kiss ass goodbye")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "add the sensu prefix" do
|
15
|
+
ExceptionHandling.sensu_prefix = "cnn_"
|
16
|
+
|
17
|
+
expect(ExceptionHandling::Sensu).to receive(:send_event).with(name: "cnn_world_is_ending", output: "stick head between knees and kiss ass goodbye", status: 1)
|
18
|
+
|
19
|
+
ExceptionHandling::Sensu.generate_event("world_is_ending", "stick head between knees and kiss ass goodbye")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "allow the level to be set to critical" do
|
23
|
+
expect(ExceptionHandling::Sensu).to receive(:send_event).with(name: "world_is_ending", output: "stick head between knees and kiss ass goodbye", status: 2)
|
24
|
+
|
25
|
+
ExceptionHandling::Sensu.generate_event("world_is_ending", "stick head between knees and kiss ass goodbye", :critical)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "error if an invalid level is supplied" do
|
29
|
+
expect(ExceptionHandling::Sensu).to_not receive(:send_event)
|
30
|
+
|
31
|
+
expect do
|
32
|
+
ExceptionHandling::Sensu.generate_event("world_is_ending", "stick head between knees and kiss ass goodbye", :hair_on_fire)
|
33
|
+
end.to raise_exception(RuntimeError, /Invalid alert level/)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "#send_event" do
|
38
|
+
before do
|
39
|
+
@event = { name: "world_is_ending", output: "stick head between knees and kiss ass goodbye", status: 1 }
|
40
|
+
@socket = SocketStub.new
|
41
|
+
end
|
42
|
+
|
43
|
+
it "send event json to sensu client" do
|
44
|
+
expect_any_instance_of(Addrinfo).to receive(:connect).with(any_args) { @socket }
|
45
|
+
ExceptionHandling::Sensu.send_event(@event)
|
46
|
+
|
47
|
+
expect(@socket.sent.first).to eq(@event.to_json)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require File.expand_path('../
|
3
|
+
require File.expand_path('../spec_helper', __dir__)
|
4
4
|
require_test_helper 'controller_helpers'
|
5
5
|
require_test_helper 'exception_helpers'
|
6
6
|
|
7
|
-
|
7
|
+
describe ExceptionHandling do
|
8
8
|
include ControllerHelpers
|
9
9
|
include ExceptionHelpers
|
10
10
|
|
11
|
-
|
11
|
+
before do
|
12
12
|
@fail_count = 0
|
13
13
|
end
|
14
14
|
|
@@ -108,194 +108,228 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
108
108
|
end
|
109
109
|
|
110
110
|
context "with warn and honeybadger notify stubbed" do
|
111
|
-
|
112
|
-
|
113
|
-
|
111
|
+
before do
|
112
|
+
allow(ExceptionHandling).to receive(:warn).with(anything)
|
113
|
+
allow(Honeybadger).to receive(:notify).with(anything)
|
114
114
|
end
|
115
115
|
|
116
116
|
context "with logger stashed" do
|
117
|
-
|
118
|
-
|
117
|
+
before { @original_logger = ExceptionHandling.logger }
|
118
|
+
after { ExceptionHandling.logger = @original_logger }
|
119
119
|
|
120
|
-
|
120
|
+
it "store logger as-is if it has ContextualLogger::Mixin" do
|
121
121
|
logger = Logger.new('/dev/null')
|
122
122
|
logger.extend(ContextualLogger::LoggerMixin)
|
123
123
|
ancestors = logger.singleton_class.ancestors.*.name
|
124
124
|
|
125
125
|
ExceptionHandling.logger = logger
|
126
|
-
|
126
|
+
expect(ExceptionHandling.logger.singleton_class.ancestors.*.name).to eq(ancestors)
|
127
127
|
end
|
128
128
|
|
129
|
-
|
130
|
-
|
129
|
+
it "allow logger = nil (no deprecation warning)" do
|
130
|
+
expect(STDERR).to receive(:puts).with(/DEPRECATION WARNING/).never
|
131
131
|
ExceptionHandling.logger = nil
|
132
132
|
end
|
133
133
|
|
134
|
-
|
135
|
-
|
134
|
+
it "[deprecated] mix in ContextualLogger::Mixin if not there" do
|
135
|
+
expect(STDERR).to receive(:puts).with(/DEPRECATION WARNING: implicit extend with ContextualLogger::LoggerMixin is deprecated and will be removed from exception_handling 3\.0/)
|
136
136
|
logger = Logger.new('/dev/null')
|
137
137
|
ancestors = logger.singleton_class.ancestors.*.name
|
138
138
|
|
139
139
|
ExceptionHandling.logger = logger
|
140
|
-
|
141
|
-
|
140
|
+
expect(ExceptionHandling.logger.singleton_class.ancestors.*.name).to_not eq(ancestors)
|
141
|
+
expect(ExceptionHandling.logger).to be_kind_of(ContextualLogger::LoggerMixin)
|
142
142
|
end
|
143
143
|
end
|
144
144
|
|
145
145
|
context "#log_error" do
|
146
|
-
|
146
|
+
it "take in additional logging context hash and pass it to the logger" do
|
147
147
|
ExceptionHandling.log_error('This is an Error', 'This is the prefix context', service_name: 'exception_handling')
|
148
|
-
|
149
|
-
|
150
|
-
|
148
|
+
expect(logged_excluding_reload_filter.last[:message]).to match(/This is an Error/)
|
149
|
+
expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
|
150
|
+
expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
|
151
|
+
end
|
152
|
+
|
153
|
+
it "log with Severity::FATAL" do
|
154
|
+
ExceptionHandling.log_error('This is a Warning', service_name: 'exception_handling')
|
155
|
+
expect('FATAL').to eq(logged_excluding_reload_filter.last[:severity])
|
151
156
|
end
|
152
157
|
end
|
153
158
|
|
154
159
|
context "#log_warning" do
|
155
|
-
|
156
|
-
|
157
|
-
|
160
|
+
it "have empty array as a backtrace" do
|
161
|
+
expect(ExceptionHandling).to receive(:log_error).with((ExceptionHandling::Warning), anything) do |error|
|
162
|
+
expect(error.backtrace).to eq([])
|
158
163
|
end
|
159
164
|
ExceptionHandling.log_warning('Now with empty array as a backtrace!')
|
160
165
|
end
|
161
166
|
|
162
|
-
|
167
|
+
it "take in additional key word args as logging context and pass them to the logger" do
|
168
|
+
ExceptionHandling.log_warning('This is a Warning', service_name: 'exception_handling')
|
169
|
+
expect(logged_excluding_reload_filter.last[:message]).to match(/This is a Warning/)
|
170
|
+
expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
|
171
|
+
expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
|
172
|
+
end
|
173
|
+
|
174
|
+
it "log with Severity::WARN" do
|
163
175
|
ExceptionHandling.log_warning('This is a Warning', service_name: 'exception_handling')
|
164
|
-
|
165
|
-
assert_not_empty logged_excluding_reload_filter.last[:context]
|
166
|
-
assert_equal logged_excluding_reload_filter.last[:context], service_name: 'exception_handling'
|
176
|
+
expect('WARN').to eq(logged_excluding_reload_filter.last[:severity])
|
167
177
|
end
|
168
178
|
end
|
169
179
|
|
170
180
|
context "#log_info" do
|
171
|
-
|
172
|
-
ExceptionHandling.
|
173
|
-
|
174
|
-
|
175
|
-
|
181
|
+
it "take in additional key word args as logging context and pass them to the logger" do
|
182
|
+
ExceptionHandling.log_info('This is an Info', service_name: 'exception_handling')
|
183
|
+
expect(logged_excluding_reload_filter.last[:message]).to match(/This is an Info/)
|
184
|
+
expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
|
185
|
+
expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
|
186
|
+
end
|
187
|
+
|
188
|
+
it "log with Severity::INFO" do
|
189
|
+
ExceptionHandling.log_info('This is a Warning', service_name: 'exception_handling')
|
190
|
+
expect('INFO').to eq(logged_excluding_reload_filter.last[:severity])
|
176
191
|
end
|
177
192
|
end
|
178
193
|
|
179
194
|
context "#log_debug" do
|
180
|
-
|
181
|
-
ExceptionHandling.
|
182
|
-
|
183
|
-
|
184
|
-
|
195
|
+
it "take in additional key word args as logging context and pass them to the logger" do
|
196
|
+
ExceptionHandling.log_debug('This is a Debug', service_name: 'exception_handling')
|
197
|
+
expect(logged_excluding_reload_filter.last[:message]).to match(/This is a Debug/)
|
198
|
+
expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
|
199
|
+
expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
|
200
|
+
end
|
201
|
+
|
202
|
+
it "log with Severity::DEBUG" do
|
203
|
+
ExceptionHandling.log_debug('This is a Warning', service_name: 'exception_handling')
|
204
|
+
expect('DEBUG').to eq(logged_excluding_reload_filter.last[:severity])
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
context "#write_exception_to_log" do
|
209
|
+
it "log warnings with Severity::WARN" do
|
210
|
+
warning = ExceptionHandling::Warning.new('This is a Warning')
|
211
|
+
ExceptionHandling.write_exception_to_log(warning, '', Time.now.to_i, service_name: 'exception_handling')
|
212
|
+
expect('WARN').to eq(logged_excluding_reload_filter.last[:severity])
|
213
|
+
end
|
214
|
+
|
215
|
+
it "log everything else with Severity::FATAL" do
|
216
|
+
error = RuntimeError.new('This is a runtime error')
|
217
|
+
ExceptionHandling.write_exception_to_log(error, '', Time.now.to_i, service_name: 'exception_handling')
|
218
|
+
expect('FATAL').to eq(logged_excluding_reload_filter.last[:severity])
|
185
219
|
end
|
186
220
|
end
|
187
221
|
|
188
222
|
context "configuration with custom_data_hook or post_log_error_hook" do
|
189
|
-
|
223
|
+
after do
|
190
224
|
ExceptionHandling.custom_data_hook = nil
|
191
225
|
ExceptionHandling.post_log_error_hook = nil
|
192
226
|
end
|
193
227
|
|
194
|
-
|
228
|
+
it "support a custom_data_hook" do
|
195
229
|
capture_notifications
|
196
230
|
|
197
231
|
ExceptionHandling.custom_data_hook = method(:append_organization_info_config)
|
198
232
|
ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
|
199
233
|
|
200
|
-
|
234
|
+
expect(sent_notifications.last.enhanced_data['user_details'].to_s).to match(/Invoca Engineering Dept./)
|
201
235
|
end
|
202
236
|
|
203
|
-
|
237
|
+
it "support a log_error hook, and pass exception_data, treat_like_warning, and logged_to_honeybadger to it" do
|
204
238
|
@honeybadger_status = nil
|
205
239
|
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
206
240
|
|
207
241
|
notify_args = []
|
208
|
-
|
242
|
+
expect(Honeybadger).to receive(:notify).with(any_args) { |info| notify_args << info; '06220c5a-b471-41e5-baeb-de247da45a56' }
|
209
243
|
ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
|
210
|
-
|
211
|
-
|
212
|
-
|
244
|
+
expect(@fail_count).to eq(1)
|
245
|
+
expect(@treat_like_warning).to eq(false)
|
246
|
+
expect(@honeybadger_status).to eq(:success)
|
213
247
|
|
214
|
-
|
215
|
-
|
216
|
-
|
248
|
+
expect(@callback_data["notes"]).to eq("this is used by a test")
|
249
|
+
expect(notify_args.size).to eq(1), notify_args.inspect
|
250
|
+
expect(notify_args.last[:context].to_s).to match(/this is used by a test/)
|
217
251
|
end
|
218
252
|
|
219
|
-
|
253
|
+
it "plumb treat_like_warning and logged_to_honeybadger to log error hook" do
|
220
254
|
@honeybadger_status = nil
|
221
255
|
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
222
256
|
ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true)
|
223
|
-
|
224
|
-
|
225
|
-
|
257
|
+
expect(@fail_count).to eq(1)
|
258
|
+
expect(@treat_like_warning).to eq(true)
|
259
|
+
expect(@honeybadger_status).to eq(:skipped)
|
226
260
|
end
|
227
261
|
|
228
|
-
|
262
|
+
it "include logging context in the exception data" do
|
229
263
|
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
230
264
|
ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true, log_context_test: "contextual_logging")
|
231
265
|
|
232
266
|
expected_log_context = {
|
233
267
|
"log_context_test" => "contextual_logging"
|
234
268
|
}
|
235
|
-
|
269
|
+
expect(@callback_data[:log_context]).to eq(expected_log_context)
|
236
270
|
end
|
237
271
|
|
238
|
-
|
272
|
+
it "support rescue exceptions from a log_error hook" do
|
239
273
|
ExceptionHandling.post_log_error_hook = method(:log_error_callback_with_failure)
|
240
274
|
log_info_messages = []
|
241
|
-
|
275
|
+
allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
|
242
276
|
log_info_messages << message
|
243
277
|
end
|
244
|
-
|
245
|
-
|
278
|
+
expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.to_not raise_error
|
279
|
+
expect(log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }).to be_truthy
|
246
280
|
end
|
247
281
|
|
248
|
-
|
282
|
+
it "handle nil message exceptions resulting from the log_error hook" do
|
249
283
|
ExceptionHandling.post_log_error_hook = method(:log_error_callback_returns_nil_message_exception)
|
250
284
|
log_info_messages = []
|
251
|
-
|
285
|
+
allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
|
252
286
|
log_info_messages << message
|
253
287
|
end
|
254
|
-
|
255
|
-
|
288
|
+
expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.to_not raise_error
|
289
|
+
expect(log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }).to be_truthy
|
256
290
|
end
|
257
291
|
|
258
|
-
|
292
|
+
it "handle nil message exceptions resulting from the custom data hook" do
|
259
293
|
ExceptionHandling.custom_data_hook = method(:custom_data_callback_returns_nil_message_exception)
|
260
294
|
log_info_messages = []
|
261
|
-
|
295
|
+
allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
|
262
296
|
log_info_messages << message
|
263
297
|
end
|
264
|
-
|
265
|
-
|
298
|
+
expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.not_to raise_error
|
299
|
+
expect(log_info_messages.find { |message| message =~ /Unable to execute custom custom_data_hook callback/ }).to be_truthy
|
266
300
|
end
|
267
301
|
end
|
268
302
|
|
269
303
|
context "Exception Handling" do
|
270
304
|
context "default_metric_name" do
|
271
305
|
context "when metric_name is present in exception_data" do
|
272
|
-
|
306
|
+
it "include metric_name in resulting metric name" do
|
273
307
|
exception = StandardError.new('this is an exception')
|
274
308
|
metric = ExceptionHandling.default_metric_name({ 'metric_name' => 'special_metric' }, exception, true)
|
275
|
-
|
309
|
+
expect(metric).to eq('exception_handling.special_metric')
|
276
310
|
end
|
277
311
|
end
|
278
312
|
|
279
313
|
context "when metric_name is not present in exception_data" do
|
280
|
-
|
314
|
+
it "return exception_handling.warning when using log warning" do
|
281
315
|
warning = ExceptionHandling::Warning.new('this is a warning')
|
282
316
|
metric = ExceptionHandling.default_metric_name({}, warning, false)
|
283
|
-
|
317
|
+
expect(metric).to eq('exception_handling.warning')
|
284
318
|
end
|
285
319
|
|
286
|
-
|
320
|
+
it "return exception_handling.exception when using log error" do
|
287
321
|
exception = StandardError.new('this is an exception')
|
288
322
|
metric = ExceptionHandling.default_metric_name({}, exception, false)
|
289
|
-
|
323
|
+
expect(metric).to eq('exception_handling.exception')
|
290
324
|
end
|
291
325
|
|
292
326
|
context "when using log error with treat_like_warning" do
|
293
|
-
|
327
|
+
it "return exception_handling.unforwarded_exception when exception not present" do
|
294
328
|
metric = ExceptionHandling.default_metric_name({}, nil, true)
|
295
|
-
|
329
|
+
expect(metric).to eq('exception_handling.unforwarded_exception')
|
296
330
|
end
|
297
331
|
|
298
|
-
|
332
|
+
it "return exception_handling.unforwarded_exception with exception classname when exception is present" do
|
299
333
|
module SomeModule
|
300
334
|
class SomeException < StandardError
|
301
335
|
end
|
@@ -303,43 +337,43 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
303
337
|
|
304
338
|
exception = SomeModule::SomeException.new('this is an exception')
|
305
339
|
metric = ExceptionHandling.default_metric_name({}, exception, true)
|
306
|
-
|
340
|
+
expect(metric).to eq('exception_handling.unforwarded_exception_SomeException')
|
307
341
|
end
|
308
342
|
end
|
309
343
|
end
|
310
344
|
end
|
311
345
|
|
312
346
|
context "default_honeybadger_metric_name" do
|
313
|
-
|
347
|
+
it "return exception_handling.honeybadger.success when status is :success" do
|
314
348
|
metric = ExceptionHandling.default_honeybadger_metric_name(:success)
|
315
|
-
|
349
|
+
expect(metric).to eq('exception_handling.honeybadger.success')
|
316
350
|
end
|
317
351
|
|
318
|
-
|
352
|
+
it "return exception_handling.honeybadger.failure when status is :failure" do
|
319
353
|
metric = ExceptionHandling.default_honeybadger_metric_name(:failure)
|
320
|
-
|
354
|
+
expect(metric).to eq('exception_handling.honeybadger.failure')
|
321
355
|
end
|
322
356
|
|
323
|
-
|
357
|
+
it "return exception_handling.honeybadger.skipped when status is :skipped" do
|
324
358
|
metric = ExceptionHandling.default_honeybadger_metric_name(:skipped)
|
325
|
-
|
359
|
+
expect(metric).to eq('exception_handling.honeybadger.skipped')
|
326
360
|
end
|
327
361
|
|
328
|
-
|
362
|
+
it "return exception_handling.honeybadger.unknown_status when status is not recognized" do
|
329
363
|
metric = ExceptionHandling.default_honeybadger_metric_name(nil)
|
330
|
-
|
364
|
+
expect(metric).to eq('exception_handling.honeybadger.unknown_status')
|
331
365
|
end
|
332
366
|
end
|
333
367
|
|
334
368
|
context "ExceptionHandling.ensure_safe" do
|
335
|
-
|
336
|
-
|
369
|
+
it "log an exception with call stack if an exception is raised." do
|
370
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
|
337
371
|
ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
|
338
372
|
end
|
339
373
|
|
340
374
|
if ActionView::VERSION::MAJOR >= 5
|
341
|
-
|
342
|
-
|
375
|
+
it "log an exception with call stack if an ActionView template exception is raised." do
|
376
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:\d+\) \nActionView::Template::Error: \(blah\):\n /, anything)
|
343
377
|
ExceptionHandling.ensure_safe do
|
344
378
|
begin
|
345
379
|
# Rails 5 made the switch from ActionView::TemplateError taking in the original exception
|
@@ -351,297 +385,292 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
351
385
|
end
|
352
386
|
end
|
353
387
|
else
|
354
|
-
|
355
|
-
|
388
|
+
it "log an exception with call stack if an ActionView template exception is raised." do
|
389
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:\d+\) \nActionView::Template::Error: \(blah\):\n /, anything)
|
356
390
|
ExceptionHandling.ensure_safe { raise ActionView::TemplateError.new({}, ArgumentError.new("blah")) }
|
357
391
|
end
|
358
392
|
end
|
359
393
|
|
360
|
-
|
361
|
-
|
394
|
+
it "should not log an exception if an exception is not raised." do
|
395
|
+
expect(ExceptionHandling.logger).to_not receive(:fatal)
|
362
396
|
ExceptionHandling.ensure_safe { ; }
|
363
397
|
end
|
364
398
|
|
365
|
-
|
366
|
-
|
399
|
+
it "return its value if used during an assignment" do
|
400
|
+
expect(ExceptionHandling.logger).to_not receive(:fatal)
|
367
401
|
b = ExceptionHandling.ensure_safe { 5 }
|
368
|
-
|
402
|
+
expect(b).to eq(5)
|
369
403
|
end
|
370
404
|
|
371
|
-
|
372
|
-
|
405
|
+
it "return nil if an exception is raised during an assignment" do
|
406
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
|
373
407
|
b = ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
|
374
|
-
|
408
|
+
expect(b).to be_nil
|
375
409
|
end
|
376
410
|
|
377
|
-
|
378
|
-
|
411
|
+
it "allow a message to be appended to the error when logged." do
|
412
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, anything)
|
379
413
|
b = ExceptionHandling.ensure_safe("mooo") { raise ArgumentError, "blah" }
|
380
|
-
|
414
|
+
expect(b).to be_nil
|
381
415
|
end
|
382
416
|
|
383
|
-
|
384
|
-
|
417
|
+
it "only rescue StandardError and descendents" do
|
418
|
+
expect { ExceptionHandling.ensure_safe("mooo") { raise Exception } }.to raise_exception(Exception)
|
385
419
|
|
386
|
-
|
420
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nStandardError: \(blah\):\n.*exception_handling_spec\.rb/, anything)
|
387
421
|
|
388
422
|
b = ExceptionHandling.ensure_safe("mooo") { raise StandardError, "blah" }
|
389
|
-
|
423
|
+
expect(b).to be_nil
|
390
424
|
end
|
391
425
|
end
|
392
426
|
|
393
427
|
context "ExceptionHandling.ensure_completely_safe" do
|
394
|
-
|
395
|
-
|
428
|
+
it "log an exception if an exception is raised." do
|
429
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
|
396
430
|
ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
|
397
431
|
end
|
398
432
|
|
399
|
-
|
400
|
-
|
433
|
+
it "should not log an exception if an exception is not raised." do
|
434
|
+
expect(ExceptionHandling.logger).to receive(:fatal).exactly(0)
|
401
435
|
ExceptionHandling.ensure_completely_safe { ; }
|
402
436
|
end
|
403
437
|
|
404
|
-
|
405
|
-
|
438
|
+
it "return its value if used during an assignment" do
|
439
|
+
expect(ExceptionHandling.logger).to receive(:fatal).exactly(0)
|
406
440
|
b = ExceptionHandling.ensure_completely_safe { 5 }
|
407
|
-
|
441
|
+
expect(b).to eq(5)
|
408
442
|
end
|
409
443
|
|
410
|
-
|
411
|
-
|
444
|
+
it "return nil if an exception is raised during an assignment" do
|
445
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything) { nil }
|
412
446
|
b = ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
|
413
|
-
|
447
|
+
expect(b).to be_nil
|
414
448
|
end
|
415
449
|
|
416
|
-
|
417
|
-
|
450
|
+
it "allow a message to be appended to the error when logged." do
|
451
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, anything)
|
418
452
|
b = ExceptionHandling.ensure_completely_safe("mooo") { raise ArgumentError, "blah" }
|
419
|
-
|
453
|
+
expect(b).to be_nil
|
420
454
|
end
|
421
455
|
|
422
|
-
|
423
|
-
|
456
|
+
it "rescue any instance or child of Exception" do
|
457
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
|
424
458
|
ExceptionHandling.ensure_completely_safe { raise Exception, "blah" }
|
425
459
|
end
|
426
460
|
|
427
|
-
|
461
|
+
it "not rescue the special exceptions that Ruby uses" do
|
428
462
|
[SystemExit, SystemStackError, NoMemoryError, SecurityError].each do |exception|
|
429
|
-
|
463
|
+
expect do
|
430
464
|
ExceptionHandling.ensure_completely_safe do
|
431
465
|
raise exception
|
432
466
|
end
|
433
|
-
end
|
467
|
+
end.to raise_exception(exception)
|
434
468
|
end
|
435
469
|
end
|
436
470
|
end
|
437
471
|
|
438
472
|
context "ExceptionHandling.ensure_escalation" do
|
439
|
-
|
473
|
+
before do
|
440
474
|
capture_notifications
|
441
475
|
ActionMailer::Base.deliveries.clear
|
442
476
|
end
|
443
477
|
|
444
|
-
|
445
|
-
|
478
|
+
it "log the exception as usual and send the proper email" do
|
479
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
|
446
480
|
ExceptionHandling.ensure_escalation("Favorite Feature") { raise ArgumentError, "blah" }
|
447
|
-
|
448
|
-
|
481
|
+
expect(ActionMailer::Base.deliveries.count).to eq(1)
|
482
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
449
483
|
|
450
484
|
email = ActionMailer::Base.deliveries.last
|
451
|
-
|
452
|
-
|
453
|
-
|
485
|
+
expect(email.subject).to eq("#{ExceptionHandling.email_environment} Escalation: Favorite Feature")
|
486
|
+
expect(email.body.to_s).to match('ArgumentError: blah')
|
487
|
+
expect(email.body.to_s).to match(ExceptionHandling.last_exception_timestamp.to_s)
|
454
488
|
end
|
455
489
|
|
456
|
-
|
457
|
-
|
490
|
+
it "should not escalate if an exception is not raised." do
|
491
|
+
expect(ExceptionHandling.logger).to_not receive(:fatal)
|
458
492
|
ExceptionHandling.ensure_escalation('Ignored') { ; }
|
459
|
-
|
493
|
+
expect(ActionMailer::Base.deliveries.count).to eq(0)
|
460
494
|
end
|
461
495
|
|
462
|
-
|
463
|
-
|
464
|
-
mock(message).deliver { raise RuntimeError.new, "Delivery Error" }
|
465
|
-
end
|
496
|
+
it "log if the escalation email cannot be sent" do
|
497
|
+
expect_any_instance_of(Mail::Message).to receive(:deliver).and_raise(RuntimeError.new, "Delivery Error")
|
466
498
|
log_fatals = []
|
467
|
-
|
468
|
-
|
499
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(any_args).at_least(:once) do |*args|
|
500
|
+
log_fatals << args
|
469
501
|
end
|
470
502
|
|
471
503
|
ExceptionHandling.ensure_escalation("ensure context") { raise ArgumentError, "first_test_exception" }
|
504
|
+
expect(log_fatals[0].first).to match(/ArgumentError.*first_test_exception/)
|
505
|
+
expect(log_fatals[1].first).to match(/safe_email_deliver.*Delivery Error/m)
|
472
506
|
|
473
|
-
|
474
|
-
assert_match(/safe_email_deliver.*Delivery Error/, log_fatals[1].first)
|
475
|
-
|
476
|
-
assert_equal 2, log_fatals.size, log_fatals.inspect
|
507
|
+
expect(log_fatals.size).to eq(2), log_fatals.inspect
|
477
508
|
|
478
|
-
|
509
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect # still sent to honeybadger
|
479
510
|
end
|
480
511
|
|
481
|
-
|
512
|
+
it "allow the caller to specify custom recipients" do
|
482
513
|
custom_recipients = ['something@invoca.com']
|
483
|
-
|
514
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
|
484
515
|
ExceptionHandling.ensure_escalation("Favorite Feature", custom_recipients) { raise ArgumentError, "blah" }
|
485
|
-
|
486
|
-
|
516
|
+
expect(ActionMailer::Base.deliveries.count).to eq(1)
|
517
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
487
518
|
|
488
519
|
email = ActionMailer::Base.deliveries.last
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
520
|
+
expect(email.subject).to eq("#{ExceptionHandling.email_environment} Escalation: Favorite Feature")
|
521
|
+
expect(email.body.to_s).to match('ArgumentError: blah')
|
522
|
+
expect(email.body.to_s).to match(ExceptionHandling.last_exception_timestamp.to_s)
|
523
|
+
expect(email.to).to eq(custom_recipients)
|
493
524
|
end
|
494
525
|
end
|
495
526
|
|
496
527
|
context "ExceptionHandling.ensure_alert" do
|
497
|
-
|
498
|
-
|
499
|
-
|
528
|
+
it "log the exception as usual and fire a sensu event" do
|
529
|
+
expect(ExceptionHandling::Sensu).to receive(:generate_event).with("Favorite Feature", "test context\nblah")
|
530
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, anything)
|
500
531
|
ExceptionHandling.ensure_alert('Favorite Feature', 'test context') { raise ArgumentError, "blah" }
|
501
532
|
end
|
502
533
|
|
503
|
-
|
504
|
-
|
505
|
-
|
534
|
+
it "should not send sensu event if an exception is not raised." do
|
535
|
+
expect(ExceptionHandling.logger).to_not receive(:fatal)
|
536
|
+
expect(ExceptionHandling::Sensu).to_not receive(:generate_event)
|
506
537
|
ExceptionHandling.ensure_alert('Ignored', 'test context') { ; }
|
507
538
|
end
|
508
539
|
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
logger.fatal(/Failed to send/, anything)
|
514
|
-
end
|
540
|
+
it "log if the sensu event could not be sent" do
|
541
|
+
expect(ExceptionHandling::Sensu).to receive(:send_event).with(anything) { raise "Failed to send" }
|
542
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/first_test_exception/, anything)
|
543
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/Failed to send/, anything)
|
515
544
|
ExceptionHandling.ensure_alert("Not Used", 'test context') { raise ArgumentError, "first_test_exception" }
|
516
545
|
end
|
517
546
|
|
518
|
-
|
519
|
-
|
547
|
+
it "log if the exception message is nil" do
|
548
|
+
expect(ExceptionHandling::Sensu).to receive(:generate_event).with("some alert", "test context\n")
|
520
549
|
ExceptionHandling.ensure_alert('some alert', 'test context') { raise_exception_with_nil_message }
|
521
550
|
end
|
522
551
|
end
|
523
552
|
|
524
553
|
context "ExceptionHandling.escalate_to_production_support" do
|
525
|
-
|
554
|
+
it "notify production support" do
|
526
555
|
subject = "Runtime Error found!"
|
527
556
|
exception = RuntimeError.new("Test")
|
528
557
|
recipients = ["prodsupport@example.com"]
|
529
558
|
|
530
|
-
|
531
|
-
|
559
|
+
expect(ExceptionHandling).to receive(:production_support_recipients).and_return(recipients).exactly(2)
|
560
|
+
expect(ExceptionHandling).to receive(:escalate).with(subject, exception, ExceptionHandling.last_exception_timestamp, recipients)
|
532
561
|
ExceptionHandling.escalate_to_production_support(exception, subject)
|
533
562
|
end
|
534
563
|
end
|
535
564
|
|
536
565
|
context "exception timestamp" do
|
537
|
-
|
566
|
+
before do
|
538
567
|
Time.now_override = Time.parse('1986-5-21 4:17 am UTC')
|
539
568
|
end
|
540
569
|
|
541
|
-
|
570
|
+
it "include the timestamp when the exception is logged" do
|
542
571
|
capture_notifications
|
543
572
|
|
544
|
-
|
573
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:517033020\) context\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, anything)
|
545
574
|
b = ExceptionHandling.ensure_safe("context") { raise ArgumentError, "blah" }
|
546
|
-
|
575
|
+
expect(b).to be_nil
|
547
576
|
|
548
|
-
|
577
|
+
expect(ExceptionHandling.last_exception_timestamp).to eq(517_033_020)
|
549
578
|
|
550
|
-
|
579
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
551
580
|
|
552
|
-
|
581
|
+
expect(sent_notifications.last.enhanced_data['timestamp']).to eq(517_033_020)
|
553
582
|
end
|
554
583
|
end
|
555
584
|
|
556
|
-
|
585
|
+
it "log the error if the exception message is nil" do
|
557
586
|
capture_notifications
|
558
587
|
|
559
588
|
ExceptionHandling.log_error(exception_with_nil_message)
|
560
589
|
|
561
|
-
|
562
|
-
|
590
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
591
|
+
expect(sent_notifications.last.enhanced_data['error_string']).to eq('RuntimeError: ')
|
563
592
|
end
|
564
593
|
|
565
|
-
|
594
|
+
it "log the error if the exception message is nil and the exception context is a hash" do
|
566
595
|
capture_notifications
|
567
596
|
|
568
597
|
ExceptionHandling.log_error(exception_with_nil_message, "SERVER_NAME" => "exceptional.com")
|
569
598
|
|
570
|
-
|
571
|
-
|
599
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
600
|
+
expect(sent_notifications.last.enhanced_data['error_string']).to eq('RuntimeError: ')
|
572
601
|
end
|
573
602
|
|
574
603
|
context "Honeybadger integration" do
|
575
604
|
context "with Honeybadger not defined" do
|
576
|
-
|
577
|
-
|
605
|
+
before do
|
606
|
+
allow(ExceptionHandling).to receive(:honeybadger_defined?) { false }
|
578
607
|
end
|
579
608
|
|
580
|
-
|
581
|
-
|
609
|
+
it "not invoke send_exception_to_honeybadger when log_error is executed" do
|
610
|
+
expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
|
582
611
|
ExceptionHandling.log_error(exception_1)
|
583
612
|
end
|
584
613
|
|
585
|
-
|
586
|
-
|
614
|
+
it "not invoke send_exception_to_honeybadger when ensure_safe is executed" do
|
615
|
+
expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
|
587
616
|
ExceptionHandling.ensure_safe { raise exception_1 }
|
588
617
|
end
|
589
618
|
end
|
590
619
|
|
591
620
|
context "with Honeybadger defined" do
|
592
|
-
|
593
|
-
|
621
|
+
it "not send_exception_to_honeybadger when log_warning is executed" do
|
622
|
+
expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
|
594
623
|
ExceptionHandling.log_warning("This should not go to honeybadger")
|
595
624
|
end
|
596
625
|
|
597
|
-
|
598
|
-
|
626
|
+
it "not send_exception_to_honeybadger when log_error is called with a Warning" do
|
627
|
+
expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
|
599
628
|
ExceptionHandling.log_error(ExceptionHandling::Warning.new("This should not go to honeybadger"))
|
600
629
|
end
|
601
630
|
|
602
|
-
|
603
|
-
|
631
|
+
it "invoke send_exception_to_honeybadger when log_error is executed" do
|
632
|
+
expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
|
604
633
|
ExceptionHandling.log_error(exception_1)
|
605
634
|
end
|
606
635
|
|
607
|
-
|
608
|
-
|
636
|
+
it "invoke send_exception_to_honeybadger when log_error_rack is executed" do
|
637
|
+
expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
|
609
638
|
ExceptionHandling.log_error_rack(exception_1, {}, nil)
|
610
639
|
end
|
611
640
|
|
612
|
-
|
613
|
-
|
641
|
+
it "invoke send_exception_to_honeybadger when ensure_safe is executed" do
|
642
|
+
expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
|
614
643
|
ExceptionHandling.ensure_safe { raise exception_1 }
|
615
644
|
end
|
616
645
|
|
617
|
-
|
618
|
-
|
619
|
-
|
646
|
+
it "specify error message as an empty string when notifying honeybadger if exception message is nil" do
|
647
|
+
expect(Honeybadger).to receive(:notify).with(any_args) do |args|
|
648
|
+
expect(args[:error_message]).to eq("")
|
620
649
|
end
|
621
650
|
ExceptionHandling.log_error(exception_with_nil_message)
|
622
651
|
end
|
623
652
|
|
624
653
|
context "with stubbed values" do
|
625
|
-
|
654
|
+
before do
|
626
655
|
Time.now_override = Time.now
|
627
656
|
@env = { server: "fe98" }
|
628
657
|
@parameters = { advertiser_id: 435, controller: "some_controller" }
|
629
658
|
@session = { username: "jsmith" }
|
630
659
|
@request_uri = "host/path"
|
631
660
|
@controller = create_dummy_controller(@env, @parameters, @session, @request_uri)
|
632
|
-
|
661
|
+
allow(ExceptionHandling).to receive(:server_name) { "invoca_fe98" }
|
633
662
|
|
634
663
|
@exception = StandardError.new("Some Exception")
|
635
664
|
@exception.set_backtrace([
|
636
|
-
"
|
637
|
-
"
|
665
|
+
"spec/unit/exception_handling_spec.rb:847:in `exception_1'",
|
666
|
+
"spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
|
638
667
|
])
|
639
668
|
@exception_context = { "SERVER_NAME" => "exceptional.com" }
|
640
669
|
end
|
641
670
|
|
642
|
-
|
671
|
+
it "send error details and relevant context data to Honeybadger with log_context" do
|
643
672
|
honeybadger_data = nil
|
644
|
-
|
673
|
+
expect(Honeybadger).to receive(:notify).with(any_args) do |data|
|
645
674
|
honeybadger_data = data
|
646
675
|
end
|
647
676
|
ExceptionHandling.logger.global_context = { service_name: "rails", region: "AWS-us-east-1" }
|
@@ -679,19 +708,19 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
679
708
|
"SERVER_NAME" => "exceptional.com"
|
680
709
|
},
|
681
710
|
backtrace: [
|
682
|
-
"
|
683
|
-
"
|
711
|
+
"spec/unit/exception_handling_spec.rb:847:in `exception_1'",
|
712
|
+
"spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
|
684
713
|
],
|
685
714
|
event_response: "Event successfully received",
|
686
715
|
log_context: { "service_name" => "bin/console", "region" => "AWS-us-east-1", "log_source" => "gem/listen" }
|
687
716
|
}
|
688
717
|
}
|
689
|
-
|
718
|
+
expect(honeybadger_data).to eq(expected_data)
|
690
719
|
end
|
691
720
|
|
692
|
-
|
721
|
+
it "send error details and relevant context data to Honeybadger with empty log_context" do
|
693
722
|
honeybadger_data = nil
|
694
|
-
|
723
|
+
expect(Honeybadger).to receive(:notify).with(any_args) do |data|
|
695
724
|
honeybadger_data = data
|
696
725
|
end
|
697
726
|
ExceptionHandling.logger.global_context = {}
|
@@ -729,22 +758,22 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
729
758
|
"SERVER_NAME" => "exceptional.com"
|
730
759
|
},
|
731
760
|
backtrace: [
|
732
|
-
"
|
733
|
-
"
|
761
|
+
"spec/unit/exception_handling_spec.rb:847:in `exception_1'",
|
762
|
+
"spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
|
734
763
|
],
|
735
764
|
event_response: "Event successfully received"
|
736
765
|
}
|
737
766
|
}
|
738
|
-
|
767
|
+
expect(honeybadger_data).to eq(expected_data)
|
739
768
|
end
|
740
769
|
end
|
741
770
|
|
742
771
|
context "with post_log_error_hook set" do
|
743
|
-
|
772
|
+
after do
|
744
773
|
ExceptionHandling.post_log_error_hook = nil
|
745
774
|
end
|
746
775
|
|
747
|
-
|
776
|
+
it "not send notification to honeybadger when exception description has the flag turned off and call log error callback with logged_to_honeybadger set to nil" do
|
748
777
|
@honeybadger_status = nil
|
749
778
|
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
750
779
|
filter_list = {
|
@@ -753,37 +782,37 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
753
782
|
send_to_honeybadger: false
|
754
783
|
}
|
755
784
|
}
|
756
|
-
|
757
|
-
|
785
|
+
allow(File).to receive(:mtime) { incrementing_mtime }
|
786
|
+
expect(YAML).to receive(:load_file).with(any_args) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }.at_least(1)
|
758
787
|
|
759
|
-
|
760
|
-
|
788
|
+
expect(ExceptionHandling).to receive(:send_exception_to_honeybadger_unless_filtered).with(any_args).exactly(1).and_call_original
|
789
|
+
expect(Honeybadger).to_not receive(:notify)
|
761
790
|
ExceptionHandling.log_error(StandardError.new("suppress Honeybadger notification"))
|
762
|
-
|
791
|
+
expect(@honeybadger_status).to eq(:skipped)
|
763
792
|
end
|
764
793
|
|
765
|
-
|
794
|
+
it "call log error callback with logged_to_honeybadger set to false if an error occurs while attempting to notify honeybadger" do
|
766
795
|
@honeybadger_status = nil
|
767
796
|
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
768
|
-
|
797
|
+
expect(Honeybadger).to receive(:notify).with(any_args) { raise "Honeybadger Notification Failure" }
|
769
798
|
ExceptionHandling.log_error(exception_1)
|
770
|
-
|
799
|
+
expect(@honeybadger_status).to eq(:failure)
|
771
800
|
end
|
772
801
|
|
773
|
-
|
802
|
+
it "call log error callback with logged_to_honeybadger set to false on unsuccessful honeybadger notification" do
|
774
803
|
@honeybadger_status = nil
|
775
804
|
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
776
|
-
|
805
|
+
expect(Honeybadger).to receive(:notify).with(any_args) { false }
|
777
806
|
ExceptionHandling.log_error(exception_1)
|
778
|
-
|
807
|
+
expect(@honeybadger_status).to eq(:failure)
|
779
808
|
end
|
780
809
|
|
781
|
-
|
810
|
+
it "call log error callback with logged_to_honeybadger set to true on successful honeybadger notification" do
|
782
811
|
@honeybadger_status = nil
|
783
812
|
ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
|
784
|
-
|
813
|
+
expect(Honeybadger).to receive(:notify).with(any_args) { '06220c5a-b471-41e5-baeb-de247da45a56' }
|
785
814
|
ExceptionHandling.log_error(exception_1)
|
786
|
-
|
815
|
+
expect(@honeybadger_status).to eq(:success)
|
787
816
|
end
|
788
817
|
end
|
789
818
|
end
|
@@ -795,33 +824,33 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
795
824
|
end
|
796
825
|
end
|
797
826
|
|
798
|
-
|
827
|
+
it "allow sections to have data with just a to_s method" do
|
799
828
|
capture_notifications
|
800
829
|
|
801
830
|
ExceptionHandling.log_error("This is my RingSwitch example.") do |data|
|
802
831
|
data.merge!(event_response: EventResponse.new)
|
803
832
|
end
|
804
833
|
|
805
|
-
|
806
|
-
|
834
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
835
|
+
expect(sent_notifications.last.enhanced_data['event_response'].to_s).to match(/message from to_s!/)
|
807
836
|
end
|
808
837
|
end
|
809
838
|
|
810
|
-
|
839
|
+
it "return the error ID (timestamp)" do
|
811
840
|
result = ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
|
812
|
-
|
841
|
+
expect(result).to eq(ExceptionHandling.last_exception_timestamp)
|
813
842
|
end
|
814
843
|
|
815
|
-
|
816
|
-
|
817
|
-
|
844
|
+
it "rescue exceptions that happen in log_error" do
|
845
|
+
allow(ExceptionHandling).to receive(:make_exception) { raise ArgumentError, "Bad argument" }
|
846
|
+
expect(ExceptionHandling).to receive(:write_exception_to_log).with(satisfy { |ex| ex.to_s['Bad argument'] },
|
818
847
|
satisfy { |context| context['ExceptionHandlingError: log_error rescued exception while logging Runtime message'] },
|
819
848
|
anything)
|
820
849
|
ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
|
821
850
|
end
|
822
851
|
|
823
|
-
|
824
|
-
|
852
|
+
it "rescue exceptions that happen when log_error yields" do
|
853
|
+
expect(ExceptionHandling).to receive(:write_exception_to_log).with(satisfy { |ex| ex.to_s['Bad argument'] },
|
825
854
|
satisfy { |context| context['Context message'] },
|
826
855
|
anything,
|
827
856
|
anything)
|
@@ -829,38 +858,38 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
829
858
|
end
|
830
859
|
|
831
860
|
context "Exception Filtering" do
|
832
|
-
|
861
|
+
before do
|
833
862
|
filter_list = { exception1: { 'error' => "my error message" },
|
834
863
|
exception2: { 'error' => "some other message", :session => "misc data" } }
|
835
|
-
|
864
|
+
allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
|
836
865
|
|
837
866
|
# bump modified time up to get the above filter loaded
|
838
|
-
|
867
|
+
allow(File).to receive(:mtime) { incrementing_mtime }
|
839
868
|
end
|
840
869
|
|
841
|
-
|
842
|
-
|
870
|
+
it "handle case where filter list is not found" do
|
871
|
+
allow(YAML).to receive(:load_file) { raise Errno::ENOENT, "File not found" }
|
843
872
|
|
844
873
|
capture_notifications
|
845
874
|
|
846
875
|
ExceptionHandling.log_error("My error message is in list")
|
847
|
-
|
876
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
848
877
|
end
|
849
878
|
|
850
|
-
|
879
|
+
it "log exception and suppress email when exception is on filter list" do
|
851
880
|
capture_notifications
|
852
881
|
|
853
882
|
ExceptionHandling.log_error("Error message is not in list")
|
854
|
-
|
883
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
855
884
|
|
856
885
|
sent_notifications.clear
|
857
886
|
ExceptionHandling.log_error("My error message is in list")
|
858
|
-
|
887
|
+
expect(sent_notifications.size).to eq(0), sent_notifications.inspect
|
859
888
|
end
|
860
889
|
|
861
|
-
|
890
|
+
it "allow filtering exception on any text in exception data" do
|
862
891
|
filters = { exception1: { session: "data: my extra session data" } }
|
863
|
-
|
892
|
+
allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filters) }
|
864
893
|
|
865
894
|
capture_notifications
|
866
895
|
|
@@ -870,7 +899,7 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
870
899
|
data: "my extra session data"
|
871
900
|
}
|
872
901
|
end
|
873
|
-
|
902
|
+
expect(sent_notifications.size).to eq(0), sent_notifications.inspect
|
874
903
|
|
875
904
|
ExceptionHandling.log_error("No match here") do |data|
|
876
905
|
data[:session] = {
|
@@ -878,86 +907,86 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
878
907
|
data: "my extra session <no match!> data"
|
879
908
|
}
|
880
909
|
end
|
881
|
-
|
910
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
882
911
|
end
|
883
912
|
|
884
|
-
|
913
|
+
it "reload filter list on the next exception if file was modified" do
|
885
914
|
capture_notifications
|
886
915
|
|
887
916
|
ExceptionHandling.log_error("Error message is not in list")
|
888
|
-
|
917
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
889
918
|
|
890
919
|
filter_list = { exception1: { 'error' => "Error message is not in list" } }
|
891
|
-
|
892
|
-
|
920
|
+
allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
|
921
|
+
allow(File).to receive(:mtime) { incrementing_mtime }
|
893
922
|
|
894
923
|
sent_notifications.clear
|
895
924
|
ExceptionHandling.log_error("Error message is not in list")
|
896
|
-
|
925
|
+
expect(sent_notifications.size).to eq(0), sent_notifications.inspect
|
897
926
|
end
|
898
927
|
|
899
|
-
|
928
|
+
it "not consider filter if both error message and body do not match" do
|
900
929
|
capture_notifications
|
901
930
|
|
902
931
|
# error message matches, but not full text
|
903
932
|
ExceptionHandling.log_error("some other message")
|
904
|
-
|
933
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
905
934
|
|
906
935
|
# now both match
|
907
936
|
sent_notifications.clear
|
908
937
|
ExceptionHandling.log_error("some other message") do |data|
|
909
938
|
data[:session] = { some_random_key: "misc data" }
|
910
939
|
end
|
911
|
-
|
940
|
+
expect(sent_notifications.size).to eq(0), sent_notifications.inspect
|
912
941
|
end
|
913
942
|
|
914
|
-
|
943
|
+
it "skip environment keys not on whitelist" do
|
915
944
|
capture_notifications
|
916
945
|
|
917
946
|
ExceptionHandling.log_error("some message") do |data|
|
918
947
|
data[:environment] = { SERVER_PROTOCOL: "HTTP/1.0", RAILS_SECRETS_YML_CONTENTS: 'password: VERY_SECRET_PASSWORD' }
|
919
948
|
end
|
920
|
-
|
949
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
921
950
|
|
922
951
|
mail = sent_notifications.last
|
923
952
|
environment = mail.enhanced_data['environment']
|
924
953
|
|
925
|
-
|
926
|
-
|
954
|
+
expect(environment["RAILS_SECRETS_YML_CONTENTS"]).to be_nil, environment.inspect # this is not on whitelist).to be_nil
|
955
|
+
expect(environment["SERVER_PROTOCOL"]).to be_truthy, environment.inspect # this is
|
927
956
|
end
|
928
957
|
|
929
|
-
|
958
|
+
it "omit environment defaults" do
|
930
959
|
capture_notifications
|
931
960
|
|
932
|
-
|
961
|
+
allow(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(anything) { |exception_info| sent_notifications << exception_info }
|
933
962
|
|
934
963
|
ExceptionHandling.log_error("some message") do |data|
|
935
964
|
data[:environment] = { SERVER_PORT: '80', SERVER_PROTOCOL: "HTTP/1.0" }
|
936
965
|
end
|
937
|
-
|
966
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
938
967
|
mail = sent_notifications.last
|
939
968
|
environment = mail.enhanced_data['environment']
|
940
969
|
|
941
|
-
|
942
|
-
|
970
|
+
expect(environment["SERVER_PORT"]).to be_nil, environment.inspect # this was default).to be_nil
|
971
|
+
expect(environment["SERVER_PROTOCOL"]).to be_truthy, environment.inspect # this was not
|
943
972
|
end
|
944
973
|
|
945
|
-
|
974
|
+
it "reject the filter file if any contain all empty regexes" do
|
946
975
|
filter_list = { exception1: { 'error' => "", :session => "" },
|
947
976
|
exception2: { 'error' => "is not in list", :session => "" } }
|
948
|
-
|
949
|
-
|
977
|
+
allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
|
978
|
+
allow(File).to receive(:mtime) { incrementing_mtime }
|
950
979
|
|
951
980
|
capture_notifications
|
952
981
|
|
953
982
|
ExceptionHandling.log_error("Error message is not in list")
|
954
|
-
|
983
|
+
expect(sent_notifications.size).to eq(1), sent_notifications.inspect
|
955
984
|
end
|
956
985
|
|
957
|
-
|
986
|
+
it "reload filter file if filename changes" do
|
958
987
|
catalog = ExceptionHandling.exception_catalog
|
959
988
|
ExceptionHandling.filter_list_filename = "./config/other_exception_filters.yml"
|
960
|
-
|
989
|
+
expect(ExceptionHandling.exception_catalog).to_not eq(catalog)
|
961
990
|
end
|
962
991
|
|
963
992
|
context "Exception Handling Mailer" do
|
@@ -971,7 +1000,7 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
971
1000
|
|
972
1001
|
[[true, false], [true, true]].each do |em_flag, synchrony_flag|
|
973
1002
|
context "eventmachine_safe = #{em_flag} && eventmachine_synchrony = #{synchrony_flag}" do
|
974
|
-
|
1003
|
+
before do
|
975
1004
|
ExceptionHandling.eventmachine_safe = em_flag
|
976
1005
|
ExceptionHandling.eventmachine_synchrony = synchrony_flag
|
977
1006
|
EventMachineStub.block = nil
|
@@ -981,61 +1010,60 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
981
1010
|
set_test_const('EventMachine::DNS::Resolver', DNSResolvStub)
|
982
1011
|
end
|
983
1012
|
|
984
|
-
|
1013
|
+
after do
|
985
1014
|
ExceptionHandling.eventmachine_safe = false
|
986
1015
|
ExceptionHandling.eventmachine_synchrony = false
|
987
1016
|
end
|
988
1017
|
|
989
|
-
|
1018
|
+
it "schedule EventMachine STMP when EventMachine defined" do
|
990
1019
|
ActionMailer::Base.deliveries.clear
|
991
1020
|
|
992
1021
|
set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
|
993
1022
|
|
994
1023
|
ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
|
995
|
-
|
1024
|
+
expect(EventMachineStub.block).to be_truthy
|
996
1025
|
EventMachineStub.block.call
|
997
|
-
|
1026
|
+
expect(DNSResolvStub.callback_block).to be_truthy
|
998
1027
|
DNSResolvStub.callback_block.call ['127.0.0.1']
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1028
|
+
expect((SmtpClientStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |_k, v| v.to_s }) .to eq(EXPECTED_SMTP_HASH), SmtpClientStub.send_hash.inspect
|
1029
|
+
expect(SmtpClientStub.last_method).to eq((synchrony_flag ? :asend : :send))
|
1030
|
+
expect(SmtpClientStub.send_hash[:content]).to match(/Exception to escalate/)
|
1002
1031
|
assert_emails 0, ActionMailer::Base.deliveries.*.to_s
|
1003
1032
|
end
|
1004
1033
|
|
1005
|
-
|
1034
|
+
it "pass the content as a proper rfc 2822 message" do
|
1006
1035
|
set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
|
1007
1036
|
ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
|
1008
|
-
|
1037
|
+
expect(EventMachineStub.block).to be_truthy
|
1009
1038
|
EventMachineStub.block.call
|
1010
|
-
|
1039
|
+
expect(DNSResolvStub.callback_block).to be_truthy
|
1011
1040
|
DNSResolvStub.callback_block.call ['127.0.0.1']
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1041
|
+
expect(content = SmtpClientStub.send_hash[:content]).to be_truthy
|
1042
|
+
expect(content).to match(/Content-Transfer-Encoding: 7bit/)
|
1043
|
+
expect(content).to match(/\r\n\.\r\n\z/)
|
1015
1044
|
end
|
1016
1045
|
|
1017
|
-
|
1046
|
+
it "log fatal on EventMachine STMP errback" do
|
1018
1047
|
ActionMailer::Base.deliveries.clear
|
1019
1048
|
|
1020
1049
|
set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientErrbackStub)
|
1021
|
-
|
1022
|
-
|
1050
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/Exception to escalate/, anything)
|
1051
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/Failed to email by SMTP: "credential mismatch"/)
|
1023
1052
|
|
1024
1053
|
ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
|
1025
|
-
|
1054
|
+
expect(EventMachineStub.block).to be_truthy
|
1026
1055
|
EventMachineStub.block.call
|
1027
|
-
|
1056
|
+
expect(DNSResolvStub.callback_block).to be_truthy
|
1028
1057
|
DNSResolvStub.callback_block.call(['127.0.0.1'])
|
1029
1058
|
SmtpClientErrbackStub.block.call("credential mismatch")
|
1030
|
-
|
1031
|
-
end
|
1059
|
+
expect((SmtpClientErrbackStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |_k, v| v.to_s }).to eq(EXPECTED_SMTP_HASH), SmtpClientErrbackStub.send_hash.inspect end
|
1032
1060
|
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1061
|
+
it "log fatal on EventMachine dns resolver errback" do
|
1062
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/Exception to escalate/, anything)
|
1063
|
+
expect(ExceptionHandling.logger).to receive(:fatal).with(/Failed to resolv DNS for localhost: "softlayer sucks"/)
|
1036
1064
|
|
1037
1065
|
ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
|
1038
|
-
|
1066
|
+
expect(EventMachineStub.block).to be_truthy
|
1039
1067
|
EventMachineStub.block.call
|
1040
1068
|
DNSResolvStub.errback_block.call("softlayer sucks")
|
1041
1069
|
end
|
@@ -1045,7 +1073,7 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
1045
1073
|
end
|
1046
1074
|
|
1047
1075
|
context "Exception mapping" do
|
1048
|
-
|
1076
|
+
before do
|
1049
1077
|
@data = {
|
1050
1078
|
environment: {
|
1051
1079
|
'HTTP_HOST' => "localhost",
|
@@ -1070,17 +1098,17 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
1070
1098
|
}
|
1071
1099
|
end
|
1072
1100
|
|
1073
|
-
|
1101
|
+
it "clean backtraces" do
|
1074
1102
|
begin
|
1075
1103
|
raise "test exception"
|
1076
1104
|
rescue => ex
|
1077
1105
|
backtrace = ex.backtrace
|
1078
1106
|
end
|
1079
1107
|
result = ExceptionHandling.send(:clean_backtrace, ex).to_s
|
1080
|
-
|
1108
|
+
expect(backtrace).to_not eq(result)
|
1081
1109
|
end
|
1082
1110
|
|
1083
|
-
|
1111
|
+
it "return entire backtrace if cleaned is emtpy" do
|
1084
1112
|
begin
|
1085
1113
|
backtrace = ["/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:312:in `find_with_ids'",
|
1086
1114
|
"/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:107:in `find'",
|
@@ -1118,12 +1146,14 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
1118
1146
|
end
|
1119
1147
|
end
|
1120
1148
|
|
1121
|
-
|
1149
|
+
rails = double(Rails)
|
1150
|
+
expect(rails).to receive(:backtrace_cleaner) { Rails::BacktraceCleaner.new }
|
1151
|
+
rails.backtrace_cleaner
|
1122
1152
|
|
1123
1153
|
ex = Exception.new
|
1124
1154
|
ex.set_backtrace(backtrace)
|
1125
1155
|
result = ExceptionHandling.send(:clean_backtrace, ex)
|
1126
|
-
|
1156
|
+
expect(result).to eq(backtrace)
|
1127
1157
|
ensure
|
1128
1158
|
Object.send(:remove_const, :Rails)
|
1129
1159
|
end
|
@@ -1131,36 +1161,36 @@ class ExceptionHandlingTest < ActiveSupport::TestCase
|
|
1131
1161
|
end
|
1132
1162
|
|
1133
1163
|
context "log_perodically" do
|
1134
|
-
|
1164
|
+
before do
|
1135
1165
|
Time.now_override = Time.now # Freeze time
|
1136
1166
|
ExceptionHandling.logger.clear
|
1137
1167
|
end
|
1138
1168
|
|
1139
|
-
|
1169
|
+
after do
|
1140
1170
|
Time.now_override = nil
|
1141
1171
|
end
|
1142
1172
|
|
1143
|
-
|
1173
|
+
it "take in additional logging context and pass them to the logger" do
|
1144
1174
|
ExceptionHandling.log_periodically(:test_context_with_periodic, 30.minutes, "this will be written", service_name: 'exception_handling')
|
1145
|
-
|
1146
|
-
|
1175
|
+
expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
|
1176
|
+
expect(logged_excluding_reload_filter.last[:context]).to eq({ service_name: 'exception_handling' })
|
1147
1177
|
end
|
1148
1178
|
|
1149
|
-
|
1179
|
+
it "log immediately when we are expected to log" do
|
1150
1180
|
ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
|
1151
|
-
|
1181
|
+
expect(logged_excluding_reload_filter.size).to eq(1)
|
1152
1182
|
|
1153
1183
|
Time.now_override = Time.now + 5.minutes
|
1154
1184
|
ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will not be written")
|
1155
|
-
|
1185
|
+
expect(logged_excluding_reload_filter.size).to eq(1)
|
1156
1186
|
|
1157
1187
|
ExceptionHandling.log_periodically(:test_another_periodic_exception, 30.minutes, "this will be written")
|
1158
|
-
|
1188
|
+
expect(logged_excluding_reload_filter.size).to eq(2)
|
1159
1189
|
|
1160
1190
|
Time.now_override = Time.now + 26.minutes
|
1161
1191
|
|
1162
1192
|
ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
|
1163
|
-
|
1193
|
+
expect(logged_excluding_reload_filter.size).to eq(3)
|
1164
1194
|
end
|
1165
1195
|
end
|
1166
1196
|
end
|