exception_handling 2.5.0.pre.1 → 2.6.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.jenkins/Jenkinsfile +24 -8
  4. data/.rspec +3 -0
  5. data/CHANGELOG.md +22 -2
  6. data/Gemfile +4 -4
  7. data/Gemfile.lock +30 -20
  8. data/Rakefile +7 -6
  9. data/lib/exception_handling.rb +8 -2
  10. data/lib/exception_handling/exception_info.rb +4 -5
  11. data/lib/exception_handling/log_stub_error.rb +2 -1
  12. data/lib/exception_handling/version.rb +1 -1
  13. data/{test → spec}/helpers/controller_helpers.rb +0 -0
  14. data/{test → spec}/helpers/exception_helpers.rb +2 -2
  15. data/spec/rake_test_warning_false.rb +20 -0
  16. data/{test/test_helper.rb → spec/spec_helper.rb} +57 -42
  17. data/spec/unit/exception_handling/exception_catalog_spec.rb +85 -0
  18. data/spec/unit/exception_handling/exception_description_spec.rb +82 -0
  19. data/{test/unit/exception_handling/exception_info_test.rb → spec/unit/exception_handling/exception_info_spec.rb} +118 -99
  20. data/{test/unit/exception_handling/honeybadger_callbacks_test.rb → spec/unit/exception_handling/honeybadger_callbacks_spec.rb} +20 -20
  21. data/{test/unit/exception_handling/log_error_stub_test.rb → spec/unit/exception_handling/log_error_stub_spec.rb} +38 -22
  22. data/{test/unit/exception_handling/mailer_test.rb → spec/unit/exception_handling/mailer_spec.rb} +17 -17
  23. data/spec/unit/exception_handling/methods_spec.rb +84 -0
  24. data/spec/unit/exception_handling/sensu_spec.rb +51 -0
  25. data/spec/unit/exception_handling_spec.rb +1226 -0
  26. metadata +34 -31
  27. data/test/unit/exception_handling/exception_catalog_test.rb +0 -85
  28. data/test/unit/exception_handling/exception_description_test.rb +0 -82
  29. data/test/unit/exception_handling/methods_test.rb +0 -84
  30. data/test/unit/exception_handling/sensu_test.rb +0 -52
  31. data/test/unit/exception_handling_test.rb +0 -1137
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exception_handling
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0.pre.1
4
+ version: 2.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Invoca
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-18 00:00:00.000000000 Z
11
+ date: 2020-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionmailer
@@ -122,6 +122,7 @@ files:
122
122
  - ".gitignore"
123
123
  - ".jenkins/Jenkinsfile"
124
124
  - ".jenkins/ruby_build_pod.yml"
125
+ - ".rspec"
125
126
  - ".rubocop.yml"
126
127
  - ".ruby-version"
127
128
  - Appraisals
@@ -148,18 +149,19 @@ files:
148
149
  - lib/exception_handling/sensu.rb
149
150
  - lib/exception_handling/testing.rb
150
151
  - lib/exception_handling/version.rb
151
- - test/helpers/controller_helpers.rb
152
- - test/helpers/exception_helpers.rb
153
- - test/test_helper.rb
154
- - test/unit/exception_handling/exception_catalog_test.rb
155
- - test/unit/exception_handling/exception_description_test.rb
156
- - test/unit/exception_handling/exception_info_test.rb
157
- - test/unit/exception_handling/honeybadger_callbacks_test.rb
158
- - test/unit/exception_handling/log_error_stub_test.rb
159
- - test/unit/exception_handling/mailer_test.rb
160
- - test/unit/exception_handling/methods_test.rb
161
- - test/unit/exception_handling/sensu_test.rb
162
- - test/unit/exception_handling_test.rb
152
+ - spec/helpers/controller_helpers.rb
153
+ - spec/helpers/exception_helpers.rb
154
+ - spec/rake_test_warning_false.rb
155
+ - spec/spec_helper.rb
156
+ - spec/unit/exception_handling/exception_catalog_spec.rb
157
+ - spec/unit/exception_handling/exception_description_spec.rb
158
+ - spec/unit/exception_handling/exception_info_spec.rb
159
+ - spec/unit/exception_handling/honeybadger_callbacks_spec.rb
160
+ - spec/unit/exception_handling/log_error_stub_spec.rb
161
+ - spec/unit/exception_handling/mailer_spec.rb
162
+ - spec/unit/exception_handling/methods_spec.rb
163
+ - spec/unit/exception_handling/sensu_spec.rb
164
+ - spec/unit/exception_handling_spec.rb
163
165
  - views/exception_handling/mailer/escalate_custom.html.erb
164
166
  - views/exception_handling/mailer/escalation_notification.html.erb
165
167
  - views/exception_handling/mailer/log_parser_exception_notification.html.erb
@@ -168,7 +170,7 @@ licenses: []
168
170
  metadata:
169
171
  source_code_uri: https://github.com/Invoca/exception_handling
170
172
  allowed_push_host: https://rubygems.org
171
- post_install_message:
173
+ post_install_message:
172
174
  rdoc_options: []
173
175
  require_paths:
174
176
  - lib
@@ -179,25 +181,26 @@ required_ruby_version: !ruby/object:Gem::Requirement
179
181
  version: '0'
180
182
  required_rubygems_version: !ruby/object:Gem::Requirement
181
183
  requirements:
182
- - - ">"
184
+ - - ">="
183
185
  - !ruby/object:Gem::Version
184
- version: 1.3.1
186
+ version: '0'
185
187
  requirements: []
186
188
  rubygems_version: 3.0.3
187
- signing_key:
189
+ signing_key:
188
190
  specification_version: 4
189
191
  summary: Invoca's exception handling logger/emailer layer, based on exception_notifier.
190
192
  Works with Rails or EventMachine or EventMachine+Synchrony.
191
193
  test_files:
192
- - test/helpers/controller_helpers.rb
193
- - test/helpers/exception_helpers.rb
194
- - test/test_helper.rb
195
- - test/unit/exception_handling/exception_catalog_test.rb
196
- - test/unit/exception_handling/exception_description_test.rb
197
- - test/unit/exception_handling/exception_info_test.rb
198
- - test/unit/exception_handling/honeybadger_callbacks_test.rb
199
- - test/unit/exception_handling/log_error_stub_test.rb
200
- - test/unit/exception_handling/mailer_test.rb
201
- - test/unit/exception_handling/methods_test.rb
202
- - test/unit/exception_handling/sensu_test.rb
203
- - test/unit/exception_handling_test.rb
194
+ - spec/helpers/controller_helpers.rb
195
+ - spec/helpers/exception_helpers.rb
196
+ - spec/rake_test_warning_false.rb
197
+ - spec/spec_helper.rb
198
+ - spec/unit/exception_handling/exception_catalog_spec.rb
199
+ - spec/unit/exception_handling/exception_description_spec.rb
200
+ - spec/unit/exception_handling/exception_info_spec.rb
201
+ - spec/unit/exception_handling/honeybadger_callbacks_spec.rb
202
+ - spec/unit/exception_handling/log_error_stub_spec.rb
203
+ - spec/unit/exception_handling/mailer_spec.rb
204
+ - spec/unit/exception_handling/methods_spec.rb
205
+ - spec/unit/exception_handling/sensu_spec.rb
206
+ - spec/unit/exception_handling_spec.rb
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require File.expand_path('../../test_helper', __dir__)
4
-
5
- module ExceptionHandling
6
- class ExceptionCatalogTest < ActiveSupport::TestCase
7
-
8
- context "With stubbed yaml content" do
9
- setup do
10
- filter_list = { exception1: { error: "my error message" },
11
- exception2: { error: "some other message", session: "misc data" } }
12
- stub(YAML).load_file { filter_list }
13
-
14
- # bump modified time up to get the above filter loaded
15
- stub(File).mtime { incrementing_mtime }
16
- end
17
-
18
- context "with loaded data" do
19
- setup do
20
- stub(File).mtime { incrementing_mtime }
21
- @exception_catalog = ExceptionCatalog.new(ExceptionHandling.filter_list_filename)
22
- @exception_catalog.send :load_file
23
- end
24
-
25
- should "have loaded filters" do
26
- assert_equal 2, @exception_catalog.instance_eval("@filters").size
27
- end
28
-
29
- should "find messages in the catalog" do
30
- assert !@exception_catalog.find(error: "Scott says unlikely to ever match")
31
- end
32
-
33
- should "find matching data" do
34
- exception_description = @exception_catalog.find(error: "this is my error message, which should match something")
35
- assert exception_description
36
- assert_equal :exception1, exception_description.filter_name
37
- end
38
- end
39
-
40
- should "write errors loading the yaml file directly to the log file" do
41
- @exception_catalog = ExceptionCatalog.new(ExceptionHandling.filter_list_filename)
42
-
43
- mock(ExceptionHandling).log_error.never
44
- mock(ExceptionHandling).write_exception_to_log(anything, "ExceptionCatalog#refresh_filters: ./config/exception_filters.yml", anything)
45
- mock(@exception_catalog).load_file { raise "noooooo" }
46
-
47
- @exception_catalog.find({})
48
- end
49
- end
50
-
51
- context "with live yaml content" do
52
- setup do
53
- @filename = File.expand_path('../../../config/exception_filters.yml', __dir__)
54
- @exception_catalog = ExceptionCatalog.new(@filename)
55
- assert_nothing_raised do
56
- @exception_catalog.send :load_file
57
- end
58
- end
59
-
60
- should "load the filter data" do
61
- assert !@exception_catalog.find(error: "Scott says unlikely to ever match")
62
- assert !@exception_catalog.find(error: "Scott says unlikely to ever match")
63
- end
64
- end
65
-
66
- context "with no yaml content" do
67
- setup do
68
- @exception_catalog = ExceptionCatalog.new(nil)
69
- end
70
-
71
- should "not load filter data" do
72
- mock(ExceptionHandling).write_exception_to_log.with_any_args.never
73
- @exception_catalog.find(error: "Scott says unlikely to ever match")
74
- end
75
- end
76
-
77
- private
78
-
79
- def incrementing_mtime
80
- @mtime ||= Time.now
81
- @mtime += 1.day
82
- end
83
-
84
- end
85
- end
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require File.expand_path('../../test_helper', __dir__)
4
-
5
- module ExceptionHandling
6
- class ExceptionDescriptionTest < ActiveSupport::TestCase
7
-
8
- context "Filter" do
9
- should "allow direct matching of strings" do
10
- @f = ExceptionDescription.new(:filter1, error: "my error message")
11
- assert @f.match?('error' => "my error message")
12
- end
13
-
14
- should "allow direct matching of strings on with symbol keys" do
15
- @f = ExceptionDescription.new(:filter1, error: "my error message")
16
- assert @f.match?(error: "my error message")
17
- end
18
-
19
- should "allow wildcards to cross line boundries" do
20
- @f = ExceptionDescription.new(:filter1, error: "my error message.*with multiple lines")
21
- assert @f.match?(error: "my error message\nwith more than one, with multiple lines")
22
- end
23
-
24
- should "complain when no regexps have a value" do
25
- assert_raise(ArgumentError, "has all blank regexe") { ExceptionDescription.new(:filter1, error: nil) }
26
- end
27
-
28
- should "report when an invalid key is passed" do
29
- assert_raise(ArgumentError, "Unknown section: not_a_parameter") { ExceptionDescription.new(:filter1, error: "my error message", not_a_parameter: false) }
30
- end
31
-
32
- should "allow send_to_honeybadger to be specified and have it disabled by default" do
33
- assert !ExceptionDescription.new(:filter1, error: "my error message", send_to_honeybadger: false).send_to_honeybadger
34
- assert ExceptionDescription.new(:filter1, error: "my error message", send_to_honeybadger: true).send_to_honeybadger
35
- assert !ExceptionDescription.new(:filter1, error: "my error message").send_to_honeybadger
36
- end
37
-
38
- should "allow send_metric to be configured" do
39
- assert !ExceptionDescription.new(:filter1, error: "my error message", send_metric: false).send_metric
40
- assert ExceptionDescription.new(:filter1, error: "my error message").send_metric
41
- end
42
-
43
- should "provide metric name" do
44
- assert_equal "filter1", ExceptionDescription.new(:filter1, error: "my error message").metric_name
45
- assert_equal "some_other_metric_name", ExceptionDescription.new(:filter1, error: "my error message", metric_name: :some_other_metric_name).metric_name
46
- end
47
-
48
- should "replace spaces in metric name" do
49
- @f = ExceptionDescription.new(:"filter has spaces", error: "my error message")
50
- assert_equal "filter_has_spaces", @f.metric_name
51
- end
52
-
53
- should "allow notes to be recorded" do
54
- assert_nil ExceptionDescription.new(:filter1, error: "my error message").notes
55
- assert_equal "a long string", ExceptionDescription.new(:filter1, error: "my error message", notes: "a long string").notes
56
- end
57
-
58
- should "not consider config options in the filter set" do
59
- assert ExceptionDescription.new(:filter1, error: "my error message", send_metric: false).match?(error: "my error message")
60
- assert ExceptionDescription.new(:filter1, error: "my error message", metric_name: "false").match?(error: "my error message")
61
- assert ExceptionDescription.new(:filter1, error: "my error message", notes: "hey").match?(error: "my error message")
62
- end
63
-
64
- should "provide exception details" do
65
- exception_description = ExceptionDescription.new(:filter1, error: "my error message", notes: "hey")
66
-
67
- expected = { "send_metric" => true, "metric_name" => "filter1", "notes" => "hey" }
68
-
69
- assert_equal expected, exception_description.exception_data
70
- end
71
-
72
- should "match multiple email addresses" do
73
- mobi = "ExceptionHandling::Warning: LoginAttempt::IPAddressLocked: failed login for 'mcc@mobistreak.com'"
74
- credit = "ExceptionHandling::Warning: LoginAttempt::IPAddressLocked: failed login for 'damon@thecreditpros.com'"
75
-
76
- exception_description = ExceptionDescription.new(:filter1, error: "ExceptionHandling::Warning: LoginAttempt::IPAddressLocked: failed login for '(mcc\@mobistreak|damon\@thecreditpros).com'")
77
- assert exception_description.match?(error: mobi), "does not match mobi"
78
- assert exception_description.match?(error: credit), "does not match credit"
79
- end
80
- end
81
- end
82
- end
@@ -1,84 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require File.expand_path('../../test_helper', __dir__)
4
-
5
- require "exception_handling/testing"
6
-
7
- module ExceptionHandling
8
- class MethodsTest < ActiveSupport::TestCase
9
- include ExceptionHelpers
10
-
11
- def dont_stub_log_error
12
- true
13
- end
14
-
15
- context "ExceptionHandling.Methods" do
16
- setup do
17
- @controller = Testing::ControllerStub.new
18
- ExceptionHandling.stub_handler = nil
19
- end
20
-
21
- should "set the around filter" do
22
- assert_equal :set_current_controller, Testing::ControllerStub.around_filter_method
23
- assert_nil ExceptionHandling.current_controller
24
- @controller.simulate_around_filter do
25
- assert_equal @controller, ExceptionHandling.current_controller
26
- end
27
- assert_nil ExceptionHandling.current_controller
28
- end
29
-
30
- should "use the current_controller when available" do
31
- capture_notifications
32
-
33
- mock(ExceptionHandling.logger).fatal(/blah/, anything)
34
- @controller.simulate_around_filter do
35
- ExceptionHandling.log_error(ArgumentError.new("blah"))
36
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
37
- assert_match(@controller.request.request_uri, sent_notifications.last.enhanced_data['request'].to_s)
38
- end
39
- end
40
-
41
- should "report long running controller action" do
42
- assert_equal 2, @controller.send(:long_controller_action_timeout)
43
- mock(ExceptionHandling).log_error(/Long controller action detected in #{@controller.class.name.split("::").last}::test_action/, anything, anything)
44
- @controller.simulate_around_filter do
45
- sleep(3)
46
- end
47
- end
48
-
49
- should "not report long running controller actions if it is less than the timeout" do
50
- assert_equal 2, @controller.send(:long_controller_action_timeout)
51
- stub(ExceptionHandling).log_error { flunk "Should not timeout" }
52
- @controller.simulate_around_filter do
53
- sleep(1)
54
- end
55
- end
56
-
57
- should "default long running controller action(300/30 for test/prod)" do
58
- class DummyController
59
- include ExceptionHandling::Methods
60
- end
61
-
62
- controller = DummyController.new
63
-
64
- Rails.env = 'production'
65
- assert_equal 30, controller.send(:long_controller_action_timeout)
66
-
67
- Rails.env = 'test'
68
- assert_equal 300, controller.send(:long_controller_action_timeout)
69
- end
70
-
71
- context "#log_warning" do
72
- should "be available to the controller" do
73
- assert_equal true, @controller.methods.include?(:log_warning)
74
- end
75
-
76
- should "call ExceptionHandling#log_warning" do
77
- mock(ExceptionHandling).log_warning("Hi mom")
78
- @controller.send(:log_warning, "Hi mom")
79
- end
80
- end
81
- end
82
-
83
- end
84
- end
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require File.expand_path('../../test_helper', __dir__)
4
-
5
- module ExceptionHandling
6
- class SensuTest < ActiveSupport::TestCase
7
- context "#generate_event" do
8
- should "create an event" do
9
- mock(ExceptionHandling::Sensu).send_event(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
- should "add the sensu prefix" do
15
- ExceptionHandling.sensu_prefix = "cnn_"
16
-
17
- mock(ExceptionHandling::Sensu).send_event(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
- should "allow the level to be set to critical" do
23
- mock(ExceptionHandling::Sensu).send_event(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
- should "error if an invalid level is supplied" do
29
- dont_allow(ExceptionHandling::Sensu).send_event
30
-
31
- assert_raise(RuntimeError, "Invalid alert level") do
32
- ExceptionHandling::Sensu.generate_event("world_is_ending", "stick head between knees and kiss ass goodbye", :hair_on_fire)
33
- end
34
- end
35
- end
36
-
37
- context "#send_event" do
38
- setup 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
- should "send event json to sensu client" do
44
- mock.any_instance_of(Addrinfo).connect.with_any_args { @socket }
45
-
46
- ExceptionHandling::Sensu.send_event(@event)
47
-
48
- assert_equal @event.to_json, @socket.sent.first
49
- end
50
- end
51
- end
52
- end
@@ -1,1137 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require File.expand_path('../test_helper', __dir__)
4
- require_test_helper 'controller_helpers'
5
- require_test_helper 'exception_helpers'
6
-
7
- class ExceptionHandlingTest < ActiveSupport::TestCase
8
- include ControllerHelpers
9
- include ExceptionHelpers
10
-
11
- setup do
12
- @fail_count = 0
13
- end
14
-
15
- def dont_stub_log_error
16
- true
17
- end
18
-
19
- def append_organization_info_config(data)
20
- data[:user_details] = {}
21
- data[:user_details][:username] = "CaryP"
22
- data[:user_details][:organization] = "Invoca Engineering Dept."
23
- rescue StandardError
24
- # don't let these out!
25
- end
26
-
27
- def custom_data_callback_returns_nil_message_exception(_data)
28
- raise_exception_with_nil_message
29
- end
30
-
31
- def log_error_callback(_data, _ex, _treat_like_warning, _honeybadger_status)
32
- @fail_count += 1
33
- end
34
-
35
- def log_error_callback_config(data, _ex, treat_like_warning, honeybadger_status)
36
- @callback_data = data
37
- @treat_like_warning = treat_like_warning
38
- @fail_count += 1
39
- @honeybadger_status = honeybadger_status
40
- end
41
-
42
- def log_error_callback_with_failure(_data, _ex)
43
- raise "this should be rescued"
44
- end
45
-
46
- def log_error_callback_returns_nil_message_exception(_data, _ex)
47
- raise_exception_with_nil_message
48
- end
49
-
50
- module EventMachineStub
51
- class << self
52
- attr_accessor :block
53
-
54
- def schedule(&block)
55
- @block = block
56
- end
57
- end
58
- end
59
-
60
- class DNSResolvStub
61
- class << self
62
- attr_accessor :callback_block
63
- attr_accessor :errback_block
64
-
65
- def resolve(_hostname)
66
- self
67
- end
68
-
69
- def callback(&block)
70
- @callback_block = block
71
- end
72
-
73
- def errback(&block)
74
- @errback_block = block
75
- end
76
- end
77
- end
78
-
79
- class SmtpClientStub
80
- class << self
81
- attr_reader :block
82
- attr_reader :last_method
83
-
84
- def errback(&block)
85
- @block = block
86
- end
87
-
88
- def send_hash
89
- @send_hash ||= {}
90
- end
91
-
92
- def send(hash)
93
- @last_method = :send
94
- send_hash.clear
95
- send_hash.merge!(hash)
96
- self
97
- end
98
-
99
- def asend(hash)
100
- send(hash)
101
- @last_method = :asend
102
- self
103
- end
104
- end
105
- end
106
-
107
- class SmtpClientErrbackStub < SmtpClientStub
108
- end
109
-
110
- context "with warn and honeybadger notify stubbed" do
111
- setup do
112
- stub(ExceptionHandling).warn(anything)
113
- stub(Honeybadger).notify(anything)
114
- end
115
-
116
- context "with logger stashed" do
117
- setup { @original_logger = ExceptionHandling.logger }
118
- teardown { ExceptionHandling.logger = @original_logger }
119
-
120
- should "store logger as-is if it has ContextualLogger::Mixin" do
121
- logger = Logger.new('/dev/null')
122
- logger.extend(ContextualLogger::LoggerMixin)
123
- ancestors = logger.singleton_class.ancestors.*.name
124
-
125
- ExceptionHandling.logger = logger
126
- assert_equal ancestors, ExceptionHandling.logger.singleton_class.ancestors.*.name
127
- end
128
-
129
- should "[deprecated] mix in ContextualLogger::Mixin if not there" do
130
- mock(STDERR).puts(/DEPRECATION WARNING: implicit extend with ContextualLogger::LoggerMixin is deprecated and will be removed from exception_handling 3\.0/)
131
- logger = Logger.new('/dev/null')
132
- ancestors = logger.singleton_class.ancestors.*.name
133
-
134
- ExceptionHandling.logger = logger
135
- assert_not_equal ancestors, ExceptionHandling.logger.singleton_class.ancestors.*.name
136
- assert_kind_of ContextualLogger::LoggerMixin, ExceptionHandling.logger
137
- end
138
- end
139
-
140
- context "#log_error" do
141
- should "take in additional logging context hash and pass it to the logger" do
142
- ExceptionHandling.log_error('This is an Error', 'This is the prefix context', service_name: 'exception_handling')
143
- assert_match(/This is an Error/, logged_excluding_reload_filter.last[:message])
144
- assert_not_empty logged_excluding_reload_filter.last[:context]
145
- assert_equal logged_excluding_reload_filter.last[:context], service_name: 'exception_handling'
146
- end
147
- end
148
-
149
- context "#log_warning" do
150
- should "have empty array as a backtrace" do
151
- mock(ExceptionHandling).log_error(is_a(ExceptionHandling::Warning), anything) do |error|
152
- assert_equal [], error.backtrace
153
- end
154
- ExceptionHandling.log_warning('Now with empty array as a backtrace!')
155
- end
156
-
157
- should "take in additional key word args as logging context and pass them to the logger" do
158
- ExceptionHandling.log_warning('This is a Warning', service_name: 'exception_handling')
159
- assert_match(/This is a Warning/, logged_excluding_reload_filter.last[:message])
160
- assert_not_empty logged_excluding_reload_filter.last[:context]
161
- assert_equal logged_excluding_reload_filter.last[:context], service_name: 'exception_handling'
162
- end
163
- end
164
-
165
- context "#log_info" do
166
- should "take in additional key word args as logging context and pass them to the logger" do
167
- ExceptionHandling.log_warning('This is an Info', service_name: 'exception_handling')
168
- assert_match(/This is an Info/, logged_excluding_reload_filter.last[:message])
169
- assert_not_empty logged_excluding_reload_filter.last[:context]
170
- assert_equal logged_excluding_reload_filter.last[:context], service_name: 'exception_handling'
171
- end
172
- end
173
-
174
- context "#log_debug" do
175
- should "take in additional key word args as logging context and pass them to the logger" do
176
- ExceptionHandling.log_warning('This is a Debug', service_name: 'exception_handling')
177
- assert_match(/This is a Debug/, logged_excluding_reload_filter.last[:message])
178
- assert_not_empty logged_excluding_reload_filter.last[:context]
179
- assert_equal logged_excluding_reload_filter.last[:context], service_name: 'exception_handling'
180
- end
181
- end
182
-
183
- context "configuration with custom_data_hook or post_log_error_hook" do
184
- teardown do
185
- ExceptionHandling.custom_data_hook = nil
186
- ExceptionHandling.post_log_error_hook = nil
187
- end
188
-
189
- should "support a custom_data_hook" do
190
- capture_notifications
191
-
192
- ExceptionHandling.custom_data_hook = method(:append_organization_info_config)
193
- ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
194
-
195
- assert_match(/Invoca Engineering Dept./, sent_notifications.last.enhanced_data['user_details'].to_s)
196
- end
197
-
198
- should "support a log_error hook, and pass exception_data, treat_like_warning, and logged_to_honeybadger to it" do
199
- @honeybadger_status = nil
200
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
201
-
202
- notify_args = []
203
- mock(Honeybadger).notify.with_any_args { |info| notify_args << info; '06220c5a-b471-41e5-baeb-de247da45a56' }
204
- ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
205
- assert_equal 1, @fail_count
206
- assert_equal false, @treat_like_warning
207
- assert_equal :success, @honeybadger_status
208
-
209
- assert_equal "this is used by a test", @callback_data["notes"]
210
- assert_equal 1, notify_args.size, notify_args.inspect
211
- assert_match(/this is used by a test/, notify_args.last[:context].to_s)
212
- end
213
-
214
- should "plumb treat_like_warning and logged_to_honeybadger to log error hook" do
215
- @honeybadger_status = nil
216
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
217
- ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true)
218
- assert_equal 1, @fail_count
219
- assert_equal true, @treat_like_warning
220
- assert_equal :skipped, @honeybadger_status
221
- end
222
-
223
- should "include logging context in the exception data" do
224
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
225
- ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true, log_context_test: "contextual_logging")
226
-
227
- expected_log_context = {
228
- "log_context_test" => "contextual_logging"
229
- }
230
- assert_equal expected_log_context, @callback_data[:log_context]
231
- end
232
-
233
- should "support rescue exceptions from a log_error hook" do
234
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_with_failure)
235
- log_info_messages = []
236
- stub(ExceptionHandling.logger).info.with_any_args do |message, _|
237
- log_info_messages << message
238
- end
239
- assert_nothing_raised { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }
240
- assert log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }
241
- end
242
-
243
- should "handle nil message exceptions resulting from the log_error hook" do
244
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_returns_nil_message_exception)
245
- log_info_messages = []
246
- stub(ExceptionHandling.logger).info.with_any_args do |message, _|
247
- log_info_messages << message
248
- end
249
- assert_nothing_raised { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }
250
- assert log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }
251
- end
252
-
253
- should "handle nil message exceptions resulting from the custom data hook" do
254
- ExceptionHandling.custom_data_hook = method(:custom_data_callback_returns_nil_message_exception)
255
- log_info_messages = []
256
- stub(ExceptionHandling.logger).info.with_any_args do |message, _|
257
- log_info_messages << message
258
- end
259
- assert_nothing_raised { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }
260
- assert log_info_messages.find { |message| message =~ /Unable to execute custom custom_data_hook callback/ }
261
- end
262
- end
263
-
264
- context "Exception Handling" do
265
- context "default_metric_name" do
266
- context "when metric_name is present in exception_data" do
267
- should "include metric_name in resulting metric name" do
268
- exception = StandardError.new('this is an exception')
269
- metric = ExceptionHandling.default_metric_name({ 'metric_name' => 'special_metric' }, exception, true)
270
- assert_equal 'exception_handling.special_metric', metric
271
- end
272
- end
273
-
274
- context "when metric_name is not present in exception_data" do
275
- should "return exception_handling.warning when using log warning" do
276
- warning = ExceptionHandling::Warning.new('this is a warning')
277
- metric = ExceptionHandling.default_metric_name({}, warning, false)
278
- assert_equal 'exception_handling.warning', metric
279
- end
280
-
281
- should "return exception_handling.exception when using log error" do
282
- exception = StandardError.new('this is an exception')
283
- metric = ExceptionHandling.default_metric_name({}, exception, false)
284
- assert_equal 'exception_handling.exception', metric
285
- end
286
-
287
- context "when using log error with treat_like_warning" do
288
- should "return exception_handling.unforwarded_exception when exception not present" do
289
- metric = ExceptionHandling.default_metric_name({}, nil, true)
290
- assert_equal 'exception_handling.unforwarded_exception', metric
291
- end
292
-
293
- should "return exception_handling.unforwarded_exception with exception classname when exception is present" do
294
- module SomeModule
295
- class SomeException < StandardError
296
- end
297
- end
298
-
299
- exception = SomeModule::SomeException.new('this is an exception')
300
- metric = ExceptionHandling.default_metric_name({}, exception, true)
301
- assert_equal 'exception_handling.unforwarded_exception_SomeException', metric
302
- end
303
- end
304
- end
305
- end
306
-
307
- context "default_honeybadger_metric_name" do
308
- should "return exception_handling.honeybadger.success when status is :success" do
309
- metric = ExceptionHandling.default_honeybadger_metric_name(:success)
310
- assert_equal 'exception_handling.honeybadger.success', metric
311
- end
312
-
313
- should "return exception_handling.honeybadger.failure when status is :failure" do
314
- metric = ExceptionHandling.default_honeybadger_metric_name(:failure)
315
- assert_equal 'exception_handling.honeybadger.failure', metric
316
- end
317
-
318
- should "return exception_handling.honeybadger.skipped when status is :skipped" do
319
- metric = ExceptionHandling.default_honeybadger_metric_name(:skipped)
320
- assert_equal 'exception_handling.honeybadger.skipped', metric
321
- end
322
-
323
- should "return exception_handling.honeybadger.unknown_status when status is not recognized" do
324
- metric = ExceptionHandling.default_honeybadger_metric_name(nil)
325
- assert_equal 'exception_handling.honeybadger.unknown_status', metric
326
- end
327
- end
328
-
329
- context "ExceptionHandling.ensure_safe" do
330
- should "log an exception with call stack if an exception is raised." do
331
- mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
332
- ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
333
- end
334
-
335
- if ActionView::VERSION::MAJOR >= 5
336
- should "log an exception with call stack if an ActionView template exception is raised." do
337
- mock(ExceptionHandling.logger).fatal(/\(Error:\d+\) ActionView::Template::Error \(blah\):\n /, anything)
338
- ExceptionHandling.ensure_safe do
339
- begin
340
- # Rails 5 made the switch from ActionView::TemplateError taking in the original exception
341
- # as an argument to using the $! global to extract the original exception
342
- raise ArgumentError, "blah"
343
- rescue
344
- raise ActionView::TemplateError.new({})
345
- end
346
- end
347
- end
348
- else
349
- should "log an exception with call stack if an ActionView template exception is raised." do
350
- mock(ExceptionHandling.logger).fatal(/\(Error:\d+\) ActionView::Template::Error \(blah\):\n /, anything)
351
- ExceptionHandling.ensure_safe { raise ActionView::TemplateError.new({}, ArgumentError.new("blah")) }
352
- end
353
- end
354
-
355
- should "should not log an exception if an exception is not raised." do
356
- dont_allow(ExceptionHandling.logger).fatal
357
- ExceptionHandling.ensure_safe { ; }
358
- end
359
-
360
- should "return its value if used during an assignment" do
361
- dont_allow(ExceptionHandling.logger).fatal
362
- b = ExceptionHandling.ensure_safe { 5 }
363
- assert_equal 5, b
364
- end
365
-
366
- should "return nil if an exception is raised during an assignment" do
367
- mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
368
- b = ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
369
- assert_nil b
370
- end
371
-
372
- should "allow a message to be appended to the error when logged." do
373
- mock(ExceptionHandling.logger).fatal(/mooo \(blah\):\n.*exception_handling_test\.rb/, anything)
374
- b = ExceptionHandling.ensure_safe("mooo") { raise ArgumentError, "blah" }
375
- assert_nil b
376
- end
377
-
378
- should "only rescue StandardError and descendents" do
379
- assert_raise(Exception) { ExceptionHandling.ensure_safe("mooo") { raise Exception } }
380
-
381
- mock(ExceptionHandling.logger).fatal(/mooo \(blah\):\n.*exception_handling_test\.rb/, anything)
382
-
383
- b = ExceptionHandling.ensure_safe("mooo") { raise StandardError, "blah" }
384
- assert_nil b
385
- end
386
- end
387
-
388
- context "ExceptionHandling.ensure_completely_safe" do
389
- should "log an exception if an exception is raised." do
390
- mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
391
- ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
392
- end
393
-
394
- should "should not log an exception if an exception is not raised." do
395
- mock(ExceptionHandling.logger).fatal.times(0)
396
- ExceptionHandling.ensure_completely_safe { ; }
397
- end
398
-
399
- should "return its value if used during an assignment" do
400
- mock(ExceptionHandling.logger).fatal.times(0)
401
- b = ExceptionHandling.ensure_completely_safe { 5 }
402
- assert_equal 5, b
403
- end
404
-
405
- should "return nil if an exception is raised during an assignment" do
406
- mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything) { nil }
407
- b = ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
408
- assert_nil b
409
- end
410
-
411
- should "allow a message to be appended to the error when logged." do
412
- mock(ExceptionHandling.logger).fatal(/mooo \(blah\):\n.*exception_handling_test\.rb/, anything)
413
- b = ExceptionHandling.ensure_completely_safe("mooo") { raise ArgumentError, "blah" }
414
- assert_nil b
415
- end
416
-
417
- should "rescue any instance or child of Exception" do
418
- mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
419
- ExceptionHandling.ensure_completely_safe { raise Exception, "blah" }
420
- end
421
-
422
- should "not rescue the special exceptions that Ruby uses" do
423
- [SystemExit, SystemStackError, NoMemoryError, SecurityError].each do |exception|
424
- assert_raise exception do
425
- ExceptionHandling.ensure_completely_safe do
426
- raise exception
427
- end
428
- end
429
- end
430
- end
431
- end
432
-
433
- context "ExceptionHandling.ensure_escalation" do
434
- setup do
435
- capture_notifications
436
- ActionMailer::Base.deliveries.clear
437
- end
438
-
439
- should "log the exception as usual and send the proper email" do
440
- mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
441
- ExceptionHandling.ensure_escalation("Favorite Feature") { raise ArgumentError, "blah" }
442
- assert_equal 1, ActionMailer::Base.deliveries.count
443
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
444
-
445
- email = ActionMailer::Base.deliveries.last
446
- assert_equal "#{ExceptionHandling.email_environment} Escalation: Favorite Feature", email.subject
447
- assert_match 'ArgumentError: blah', email.body.to_s
448
- assert_match ExceptionHandling.last_exception_timestamp.to_s, email.body.to_s
449
- end
450
-
451
- should "should not escalate if an exception is not raised." do
452
- dont_allow(ExceptionHandling.logger).fatal
453
- ExceptionHandling.ensure_escalation('Ignored') { ; }
454
- assert_equal 0, ActionMailer::Base.deliveries.count
455
- end
456
-
457
- should "log if the escalation email cannot be sent" do
458
- any_instance_of(Mail::Message) do |message|
459
- mock(message).deliver { raise RuntimeError.new, "Delivery Error" }
460
- end
461
- log_fatals = []
462
- stub(ExceptionHandling.logger) do |logger|
463
- logger.fatal.with_any_args { |*args| log_fatals << args }
464
- end
465
-
466
- ExceptionHandling.ensure_escalation("ensure context") { raise ArgumentError, "first_test_exception" }
467
-
468
- assert_match(/ArgumentError.*first_test_exception/, log_fatals[0].first)
469
- assert_match(/safe_email_deliver.*Delivery Error/, log_fatals[1].first)
470
-
471
- assert_equal 2, log_fatals.size, log_fatals.inspect
472
-
473
- assert_equal 1, sent_notifications.size, sent_notifications.inspect # still sent to honeybadger
474
- end
475
-
476
- should "allow the caller to specify custom recipients" do
477
- custom_recipients = ['something@invoca.com']
478
- mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
479
- ExceptionHandling.ensure_escalation("Favorite Feature", custom_recipients) { raise ArgumentError, "blah" }
480
- assert_equal 1, ActionMailer::Base.deliveries.count
481
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
482
-
483
- email = ActionMailer::Base.deliveries.last
484
- assert_equal "#{ExceptionHandling.email_environment} Escalation: Favorite Feature", email.subject
485
- assert_match 'ArgumentError: blah', email.body.to_s
486
- assert_match ExceptionHandling.last_exception_timestamp.to_s, email.body.to_s
487
- assert_equal custom_recipients, email.to
488
- end
489
- end
490
-
491
- context "ExceptionHandling.ensure_alert" do
492
- should "log the exception as usual and fire a sensu event" do
493
- mock(ExceptionHandling::Sensu).generate_event("Favorite Feature", "test context\nblah")
494
- mock(ExceptionHandling.logger).fatal(/\(blah\):\n.*exception_handling_test\.rb/, anything)
495
- ExceptionHandling.ensure_alert('Favorite Feature', 'test context') { raise ArgumentError, "blah" }
496
- end
497
-
498
- should "should not send sensu event if an exception is not raised." do
499
- dont_allow(ExceptionHandling.logger).fatal
500
- dont_allow(ExceptionHandling::Sensu).generate_event
501
- ExceptionHandling.ensure_alert('Ignored', 'test context') { ; }
502
- end
503
-
504
- should "log if the sensu event could not be sent" do
505
- mock(ExceptionHandling::Sensu).send_event(anything) { raise "Failed to send" }
506
- mock(ExceptionHandling.logger) do |logger|
507
- logger.fatal(/first_test_exception/, anything)
508
- logger.fatal(/Failed to send/, anything)
509
- end
510
- ExceptionHandling.ensure_alert("Not Used", 'test context') { raise ArgumentError, "first_test_exception" }
511
- end
512
-
513
- should "log if the exception message is nil" do
514
- mock(ExceptionHandling::Sensu).generate_event("some alert", "test context\n")
515
- ExceptionHandling.ensure_alert('some alert', 'test context') { raise_exception_with_nil_message }
516
- end
517
- end
518
-
519
- context "ExceptionHandling.escalate_to_production_support" do
520
- should "notify production support" do
521
- subject = "Runtime Error found!"
522
- exception = RuntimeError.new("Test")
523
- recipients = ["prodsupport@example.com"]
524
-
525
- mock(ExceptionHandling).production_support_recipients { recipients }.times(2)
526
- mock(ExceptionHandling).escalate(subject, exception, ExceptionHandling.last_exception_timestamp, recipients)
527
- ExceptionHandling.escalate_to_production_support(exception, subject)
528
- end
529
- end
530
-
531
- context "exception timestamp" do
532
- setup do
533
- Time.now_override = Time.parse('1986-5-21 4:17 am UTC')
534
- end
535
-
536
- should "include the timestamp when the exception is logged" do
537
- capture_notifications
538
-
539
- mock(ExceptionHandling.logger).fatal(/\(Error:517033020\) ArgumentError context \(blah\):\n.*exception_handling_test\.rb/, anything)
540
- b = ExceptionHandling.ensure_safe("context") { raise ArgumentError, "blah" }
541
- assert_nil b
542
-
543
- assert_equal 517_033_020, ExceptionHandling.last_exception_timestamp
544
-
545
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
546
-
547
- assert_equal 517_033_020, sent_notifications.last.enhanced_data['timestamp']
548
- end
549
- end
550
-
551
- should "log the error if the exception message is nil" do
552
- capture_notifications
553
-
554
- ExceptionHandling.log_error(exception_with_nil_message)
555
-
556
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
557
- assert_equal 'RuntimeError: ', sent_notifications.last.enhanced_data['error_string']
558
- end
559
-
560
- should "log the error if the exception message is nil and the exception context is a hash" do
561
- capture_notifications
562
-
563
- ExceptionHandling.log_error(exception_with_nil_message, "SERVER_NAME" => "exceptional.com")
564
-
565
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
566
- assert_equal 'RuntimeError: ', sent_notifications.last.enhanced_data['error_string']
567
- end
568
-
569
- context "Honeybadger integration" do
570
- context "with Honeybadger not defined" do
571
- setup do
572
- stub(ExceptionHandling).honeybadger_defined? { false }
573
- end
574
-
575
- should "not invoke send_exception_to_honeybadger when log_error is executed" do
576
- dont_allow(ExceptionHandling).send_exception_to_honeybadger
577
- ExceptionHandling.log_error(exception_1)
578
- end
579
-
580
- should "not invoke send_exception_to_honeybadger when ensure_safe is executed" do
581
- dont_allow(ExceptionHandling).send_exception_to_honeybadger
582
- ExceptionHandling.ensure_safe { raise exception_1 }
583
- end
584
- end
585
-
586
- context "with Honeybadger defined" do
587
- should "not send_exception_to_honeybadger when log_warning is executed" do
588
- dont_allow(ExceptionHandling).send_exception_to_honeybadger
589
- ExceptionHandling.log_warning("This should not go to honeybadger")
590
- end
591
-
592
- should "not send_exception_to_honeybadger when log_error is called with a Warning" do
593
- dont_allow(ExceptionHandling).send_exception_to_honeybadger
594
- ExceptionHandling.log_error(ExceptionHandling::Warning.new("This should not go to honeybadger"))
595
- end
596
-
597
- should "invoke send_exception_to_honeybadger when log_error is executed" do
598
- mock.proxy(ExceptionHandling).send_exception_to_honeybadger.with_any_args
599
- ExceptionHandling.log_error(exception_1)
600
- end
601
-
602
- should "invoke send_exception_to_honeybadger when log_error_rack is executed" do
603
- mock.proxy(ExceptionHandling).send_exception_to_honeybadger.with_any_args
604
- ExceptionHandling.log_error_rack(exception_1, {}, nil)
605
- end
606
-
607
- should "invoke send_exception_to_honeybadger when ensure_safe is executed" do
608
- mock.proxy(ExceptionHandling).send_exception_to_honeybadger.with_any_args
609
- ExceptionHandling.ensure_safe { raise exception_1 }
610
- end
611
-
612
- should "specify error message as an empty string when notifying honeybadger if exception message is nil" do
613
- mock(Honeybadger).notify.with_any_args do |args|
614
- assert_equal "", args[:error_message]
615
- end
616
- ExceptionHandling.log_error(exception_with_nil_message)
617
- end
618
-
619
- should "send error details and relevant context data to Honeybadger" do
620
- Time.now_override = Time.now
621
- env = { server: "fe98" }
622
- parameters = { advertiser_id: 435, controller: "some_controller" }
623
- session = { username: "jsmith" }
624
- request_uri = "host/path"
625
- controller = create_dummy_controller(env, parameters, session, request_uri)
626
- stub(ExceptionHandling).server_name { "invoca_fe98" }
627
-
628
- exception = StandardError.new("Some Exception")
629
- exception.set_backtrace([
630
- "test/unit/exception_handling_test.rb:847:in `exception_1'",
631
- "test/unit/exception_handling_test.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
632
- ])
633
- exception_context = { "SERVER_NAME" => "exceptional.com" }
634
-
635
- honeybadger_data = nil
636
- mock(Honeybadger).notify.with_any_args do |data|
637
- honeybadger_data = data
638
- end
639
- log_context = { log_source: "gem/listen", cuid: "AA12BC34DE" }
640
- ExceptionHandling.log_error(exception, exception_context, controller, **log_context) do |data|
641
- data[:scm_revision] = "5b24eac37aaa91f5784901e9aabcead36fd9df82"
642
- data[:user_details] = { username: "jsmith" }
643
- data[:event_response] = "Event successfully received"
644
- data[:other_section] = "This should not be included in the response"
645
- end
646
-
647
- expected_data = {
648
- error_class: :"Test Exception",
649
- error_message: "Some Exception",
650
- controller: "some_controller",
651
- exception: exception,
652
- context: {
653
- timestamp: Time.now.to_i,
654
- error_class: "StandardError",
655
- server: "invoca_fe98",
656
- exception_context: { "SERVER_NAME" => "exceptional.com" },
657
- scm_revision: "5b24eac37aaa91f5784901e9aabcead36fd9df82",
658
- notes: "this is used by a test",
659
- user_details: { "username" => "jsmith" },
660
- request: {
661
- "params" => { "advertiser_id" => 435, "controller" => "some_controller" },
662
- "rails_root" => "Rails.root not defined. Is this a test environment?",
663
- "url" => "host/path"
664
- },
665
- session: {
666
- "key" => nil,
667
- "data" => { "username" => "jsmith" }
668
- },
669
- environment: {
670
- "SERVER_NAME" => "exceptional.com"
671
- },
672
- backtrace: [
673
- "test/unit/exception_handling_test.rb:847:in `exception_1'",
674
- "test/unit/exception_handling_test.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
675
- ],
676
- event_response: "Event successfully received",
677
- log_context: { "log_source" => "gem/listen", "cuid" => "AA12BC34DE" }
678
- }
679
- }
680
- assert_equal_with_diff expected_data, honeybadger_data
681
- end
682
-
683
- context "with post_log_error_hook set" do
684
- teardown do
685
- ExceptionHandling.post_log_error_hook = nil
686
- end
687
-
688
- should "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
689
- @honeybadger_status = nil
690
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
691
- filter_list = {
692
- NoHoneybadger: {
693
- error: "suppress Honeybadger notification",
694
- send_to_honeybadger: false
695
- }
696
- }
697
- stub(File).mtime { incrementing_mtime }
698
- mock(YAML).load_file.with_any_args { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }.at_least(1)
699
-
700
- mock.proxy(ExceptionHandling).send_exception_to_honeybadger_unless_filtered.with_any_args.once
701
- dont_allow(Honeybadger).notify
702
- ExceptionHandling.log_error(StandardError.new("suppress Honeybadger notification"))
703
- assert_equal :skipped, @honeybadger_status
704
- end
705
-
706
- should "call log error callback with logged_to_honeybadger set to false if an error occurs while attempting to notify honeybadger" do
707
- @honeybadger_status = nil
708
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
709
- mock(Honeybadger).notify.with_any_args { raise "Honeybadger Notification Failure" }
710
- ExceptionHandling.log_error(exception_1)
711
- assert_equal :failure, @honeybadger_status
712
- end
713
-
714
- should "call log error callback with logged_to_honeybadger set to false on unsuccessful honeybadger notification" do
715
- @honeybadger_status = nil
716
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
717
- mock(Honeybadger).notify.with_any_args { false }
718
- ExceptionHandling.log_error(exception_1)
719
- assert_equal :failure, @honeybadger_status
720
- end
721
-
722
- should "call log error callback with logged_to_honeybadger set to true on successful honeybadger notification" do
723
- @honeybadger_status = nil
724
- ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
725
- mock(Honeybadger).notify.with_any_args { '06220c5a-b471-41e5-baeb-de247da45a56' }
726
- ExceptionHandling.log_error(exception_1)
727
- assert_equal :success, @honeybadger_status
728
- end
729
- end
730
- end
731
- end
732
-
733
- class EventResponse
734
- def to_s
735
- "message from to_s!"
736
- end
737
- end
738
-
739
- should "allow sections to have data with just a to_s method" do
740
- capture_notifications
741
-
742
- ExceptionHandling.log_error("This is my RingSwitch example.") do |data|
743
- data.merge!(event_response: EventResponse.new)
744
- end
745
-
746
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
747
- assert_match(/message from to_s!/, sent_notifications.last.enhanced_data['event_response'].to_s)
748
- end
749
- end
750
-
751
- should "return the error ID (timestamp)" do
752
- result = ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
753
- assert_equal ExceptionHandling.last_exception_timestamp, result
754
- end
755
-
756
- should "rescue exceptions that happen in log_error" do
757
- stub(ExceptionHandling).make_exception { raise ArgumentError, "Bad argument" }
758
- mock(ExceptionHandling).write_exception_to_log(satisfy { |ex| ex.to_s['Bad argument'] },
759
- satisfy { |context| context['ExceptionHandlingError: log_error rescued exception while logging Runtime message'] },
760
- anything)
761
- ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
762
- end
763
-
764
- should "rescue exceptions that happen when log_error yields" do
765
- mock(ExceptionHandling).write_exception_to_log(satisfy { |ex| ex.to_s['Bad argument'] },
766
- satisfy { |context| context['Context message'] },
767
- anything,
768
- anything)
769
- ExceptionHandling.log_error(ArgumentError.new("Bad argument"), "Context message") { |_data| raise 'Error!!!' }
770
- end
771
-
772
- context "Exception Filtering" do
773
- setup do
774
- filter_list = { exception1: { 'error' => "my error message" },
775
- exception2: { 'error' => "some other message", :session => "misc data" } }
776
- stub(YAML).load_file { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
777
-
778
- # bump modified time up to get the above filter loaded
779
- stub(File).mtime { incrementing_mtime }
780
- end
781
-
782
- should "handle case where filter list is not found" do
783
- stub(YAML).load_file { raise Errno::ENOENT, "File not found" }
784
-
785
- capture_notifications
786
-
787
- ExceptionHandling.log_error("My error message is in list")
788
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
789
- end
790
-
791
- should "log exception and suppress email when exception is on filter list" do
792
- capture_notifications
793
-
794
- ExceptionHandling.log_error("Error message is not in list")
795
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
796
-
797
- sent_notifications.clear
798
- ExceptionHandling.log_error("My error message is in list")
799
- assert_equal 0, sent_notifications.size, sent_notifications.inspect
800
- end
801
-
802
- should "allow filtering exception on any text in exception data" do
803
- filters = { exception1: { session: "data: my extra session data" } }
804
- stub(YAML).load_file { ActiveSupport::HashWithIndifferentAccess.new(filters) }
805
-
806
- capture_notifications
807
-
808
- ExceptionHandling.log_error("No match here") do |data|
809
- data[:session] = {
810
- key: "@session_id",
811
- data: "my extra session data"
812
- }
813
- end
814
- assert_equal 0, sent_notifications.size, sent_notifications.inspect
815
-
816
- ExceptionHandling.log_error("No match here") do |data|
817
- data[:session] = {
818
- key: "@session_id",
819
- data: "my extra session <no match!> data"
820
- }
821
- end
822
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
823
- end
824
-
825
- should "reload filter list on the next exception if file was modified" do
826
- capture_notifications
827
-
828
- ExceptionHandling.log_error("Error message is not in list")
829
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
830
-
831
- filter_list = { exception1: { 'error' => "Error message is not in list" } }
832
- stub(YAML).load_file { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
833
- stub(File).mtime { incrementing_mtime }
834
-
835
- sent_notifications.clear
836
- ExceptionHandling.log_error("Error message is not in list")
837
- assert_equal 0, sent_notifications.size, sent_notifications.inspect
838
- end
839
-
840
- should "not consider filter if both error message and body do not match" do
841
- capture_notifications
842
-
843
- # error message matches, but not full text
844
- ExceptionHandling.log_error("some other message")
845
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
846
-
847
- # now both match
848
- sent_notifications.clear
849
- ExceptionHandling.log_error("some other message") do |data|
850
- data[:session] = { some_random_key: "misc data" }
851
- end
852
- assert_equal 0, sent_notifications.size, sent_notifications.inspect
853
- end
854
-
855
- should "skip environment keys not on whitelist" do
856
- capture_notifications
857
-
858
- ExceptionHandling.log_error("some message") do |data|
859
- data[:environment] = { SERVER_PROTOCOL: "HTTP/1.0", RAILS_SECRETS_YML_CONTENTS: 'password: VERY_SECRET_PASSWORD' }
860
- end
861
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
862
-
863
- mail = sent_notifications.last
864
- environment = mail.enhanced_data['environment']
865
-
866
- assert_nil environment["RAILS_SECRETS_YML_CONTENTS"], environment.inspect # this is not on whitelist
867
- assert environment["SERVER_PROTOCOL"], environment.inspect # this is
868
- end
869
-
870
- should "omit environment defaults" do
871
- capture_notifications
872
-
873
- stub(ExceptionHandling).send_exception_to_honeybadger(anything) { |exception_info| sent_notifications << exception_info }
874
-
875
- ExceptionHandling.log_error("some message") do |data|
876
- data[:environment] = { SERVER_PORT: '80', SERVER_PROTOCOL: "HTTP/1.0" }
877
- end
878
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
879
- mail = sent_notifications.last
880
- environment = mail.enhanced_data['environment']
881
-
882
- assert_nil environment["SERVER_PORT"], environment.inspect # this was default
883
- assert environment["SERVER_PROTOCOL"], environment # this was not
884
- end
885
-
886
- should "reject the filter file if any contain all empty regexes" do
887
- filter_list = { exception1: { 'error' => "", :session => "" },
888
- exception2: { 'error' => "is not in list", :session => "" } }
889
- stub(YAML).load_file { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
890
- stub(File).mtime { incrementing_mtime }
891
-
892
- capture_notifications
893
-
894
- ExceptionHandling.log_error("Error message is not in list")
895
- assert_equal 1, sent_notifications.size, sent_notifications.inspect
896
- end
897
-
898
- should "reload filter file if filename changes" do
899
- catalog = ExceptionHandling.exception_catalog
900
- ExceptionHandling.filter_list_filename = "./config/other_exception_filters.yml"
901
- assert_not_equal catalog, ExceptionHandling.exception_catalog
902
- end
903
-
904
- context "Exception Handling Mailer" do
905
- EXPECTED_SMTP_HASH =
906
- {
907
- host: '127.0.0.1',
908
- domain: 'localhost.localdomain',
909
- from: 'server@example.com',
910
- to: 'escalation@example.com'
911
- }.freeze
912
-
913
- [[true, false], [true, true]].each do |em_flag, synchrony_flag|
914
- context "eventmachine_safe = #{em_flag} && eventmachine_synchrony = #{synchrony_flag}" do
915
- setup do
916
- ExceptionHandling.eventmachine_safe = em_flag
917
- ExceptionHandling.eventmachine_synchrony = synchrony_flag
918
- EventMachineStub.block = nil
919
- set_test_const('EventMachine', EventMachineStub)
920
- set_test_const('EventMachine::Protocols', Module.new)
921
- set_test_const('EventMachine::DNS', Module.new)
922
- set_test_const('EventMachine::DNS::Resolver', DNSResolvStub)
923
- end
924
-
925
- teardown do
926
- ExceptionHandling.eventmachine_safe = false
927
- ExceptionHandling.eventmachine_synchrony = false
928
- end
929
-
930
- should "schedule EventMachine STMP when EventMachine defined" do
931
- ActionMailer::Base.deliveries.clear
932
-
933
- set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
934
-
935
- ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
936
- assert EventMachineStub.block
937
- EventMachineStub.block.call
938
- assert DNSResolvStub.callback_block
939
- DNSResolvStub.callback_block.call ['127.0.0.1']
940
- assert_equal_with_diff EXPECTED_SMTP_HASH, (SmtpClientStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |_k, v| v.to_s }, SmtpClientStub.send_hash.inspect
941
- assert_equal((synchrony_flag ? :asend : :send), SmtpClientStub.last_method)
942
- assert_match(/Exception to escalate/, SmtpClientStub.send_hash[:content])
943
- assert_emails 0, ActionMailer::Base.deliveries.*.to_s
944
- end
945
-
946
- should "pass the content as a proper rfc 2822 message" do
947
- set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
948
- ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
949
- assert EventMachineStub.block
950
- EventMachineStub.block.call
951
- assert DNSResolvStub.callback_block
952
- DNSResolvStub.callback_block.call ['127.0.0.1']
953
- assert content = SmtpClientStub.send_hash[:content]
954
- assert_match(/Content-Transfer-Encoding: 7bit/, content)
955
- assert_match(/\r\n\.\r\n\z/, content)
956
- end
957
-
958
- should "log fatal on EventMachine STMP errback" do
959
- ActionMailer::Base.deliveries.clear
960
-
961
- set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientErrbackStub)
962
- mock(ExceptionHandling.logger).fatal(/Exception to escalate/, anything)
963
- mock(ExceptionHandling.logger).fatal(/Failed to email by SMTP: "credential mismatch"/)
964
-
965
- ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
966
- assert EventMachineStub.block
967
- EventMachineStub.block.call
968
- assert DNSResolvStub.callback_block
969
- DNSResolvStub.callback_block.call(['127.0.0.1'])
970
- SmtpClientErrbackStub.block.call("credential mismatch")
971
- assert_equal_with_diff EXPECTED_SMTP_HASH, (SmtpClientErrbackStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |_k, v| v.to_s }, SmtpClientErrbackStub.send_hash.inspect
972
- end
973
-
974
- should "log fatal on EventMachine dns resolver errback" do
975
- mock(ExceptionHandling.logger).fatal(/Exception to escalate/, anything)
976
- mock(ExceptionHandling.logger).fatal(/Failed to resolv DNS for localhost: "softlayer sucks"/)
977
-
978
- ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
979
- assert EventMachineStub.block
980
- EventMachineStub.block.call
981
- DNSResolvStub.errback_block.call("softlayer sucks")
982
- end
983
- end
984
- end
985
- end
986
- end
987
-
988
- context "Exception mapping" do
989
- setup do
990
- @data = {
991
- environment: {
992
- 'HTTP_HOST' => "localhost",
993
- 'HTTP_REFERER' => "http://localhost/action/controller/instance",
994
- },
995
- session: {
996
- data: {
997
- affiliate_id: defined?(Affiliate) ? Affiliate.first.id : '1',
998
- edit_mode: true,
999
- advertiser_id: defined?(Advertiser) ? Advertiser.first.id : '1',
1000
- username_id: defined?(Username) ? Username.first.id : '1',
1001
- user_id: defined?(User) ? User.first.id : '1',
1002
- flash: {},
1003
- impersonated_organization_pk: 'Advertiser_1'
1004
- }
1005
- },
1006
- request: {},
1007
- backtrace: ["[GEM_ROOT]/gems/actionpack-2.1.0/lib/action_controller/filters.rb:580:in `call_filters'", "[GEM_ROOT]/gems/actionpack-2.1.0/lib/action_controller/filters.rb:601:in `run_before_filters'"],
1008
- api_key: "none",
1009
- error_class: "StandardError",
1010
- error: 'Some error message'
1011
- }
1012
- end
1013
-
1014
- should "clean backtraces" do
1015
- begin
1016
- raise "test exception"
1017
- rescue => ex
1018
- backtrace = ex.backtrace
1019
- end
1020
- result = ExceptionHandling.send(:clean_backtrace, ex).to_s
1021
- assert_not_equal result, backtrace
1022
- end
1023
-
1024
- should "return entire backtrace if cleaned is emtpy" do
1025
- begin
1026
- backtrace = ["/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:312:in `find_with_ids'",
1027
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:107:in `find'",
1028
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `__send__'",
1029
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `find'",
1030
- "/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `call'",
1031
- "/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `test: Exception mapping should return entire backtrace if cleaned is emtpy. '",
1032
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `__send__'",
1033
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `run'",
1034
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:447:in `_run__1913317170__setup__4__callbacks'",
1035
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `send'",
1036
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `__run_callback'",
1037
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:385:in `_run_setup_callbacks'",
1038
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `send'",
1039
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `run_callbacks'",
1040
- "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:70:in `run'",
1041
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:34:in `run'",
1042
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `each'",
1043
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `run'",
1044
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnermediator.rb:46:in `old_run_suite'",
1045
- "(eval):12:in `run_suite'",
1046
- "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `send'",
1047
- "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `start_mediator'",
1048
- "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:81:in `start'",
1049
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnerutilities.rb:29:in `run'",
1050
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/autorunner.rb:12:in `run'",
1051
- "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit.rb:279",
1052
- "-e:1"]
1053
-
1054
- module ::Rails
1055
- class BacktraceCleaner
1056
- def clean(_backtrace)
1057
- []
1058
- end
1059
- end
1060
- end
1061
-
1062
- mock(Rails).backtrace_cleaner { Rails::BacktraceCleaner.new }
1063
-
1064
- ex = Exception.new
1065
- ex.set_backtrace(backtrace)
1066
- result = ExceptionHandling.send(:clean_backtrace, ex)
1067
- assert_equal backtrace, result
1068
- ensure
1069
- Object.send(:remove_const, :Rails)
1070
- end
1071
- end
1072
- end
1073
-
1074
- context "log_perodically" do
1075
- setup do
1076
- Time.now_override = Time.now # Freeze time
1077
- ExceptionHandling.logger.clear
1078
- end
1079
-
1080
- teardown do
1081
- Time.now_override = nil
1082
- end
1083
-
1084
- should "take in additional logging context and pass them to the logger" do
1085
- ExceptionHandling.log_periodically(:test_context_with_periodic, 30.minutes, "this will be written", service_name: 'exception_handling')
1086
- assert_not_empty logged_excluding_reload_filter.last[:context]
1087
- assert_equal({ service_name: 'exception_handling' }, logged_excluding_reload_filter.last[:context])
1088
- end
1089
-
1090
- should "log immediately when we are expected to log" do
1091
- ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
1092
- assert_equal 1, logged_excluding_reload_filter.size
1093
-
1094
- Time.now_override = Time.now + 5.minutes
1095
- ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will not be written")
1096
- assert_equal 1, logged_excluding_reload_filter.size
1097
-
1098
- ExceptionHandling.log_periodically(:test_another_periodic_exception, 30.minutes, "this will be written")
1099
- assert_equal 2, logged_excluding_reload_filter.size
1100
-
1101
- Time.now_override = Time.now + 26.minutes
1102
-
1103
- ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
1104
- assert_equal 3, logged_excluding_reload_filter.size
1105
- end
1106
- end
1107
- end
1108
-
1109
- private
1110
-
1111
- def logged_excluding_reload_filter
1112
- ExceptionHandling.logger.logged.select { |l| l[:message] !~ /Reloading filter list/ }
1113
- end
1114
-
1115
- def incrementing_mtime
1116
- @mtime ||= Time.now
1117
- @mtime += 1.day
1118
- end
1119
-
1120
- def exception_1
1121
- @exception_1 ||=
1122
- begin
1123
- raise StandardError, "Exception 1"
1124
- rescue => ex
1125
- ex
1126
- end
1127
- end
1128
-
1129
- def exception_2
1130
- @exception_2 ||=
1131
- begin
1132
- raise StandardError, "Exception 2"
1133
- rescue => ex
1134
- ex
1135
- end
1136
- end
1137
- end