exception_notification 4.4.0 → 4.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Appraisals +3 -1
- data/CHANGELOG.rdoc +10 -0
- data/Gemfile +2 -0
- data/README.md +34 -8
- data/Rakefile +2 -0
- data/examples/sample_app.rb +2 -0
- data/examples/sinatra/Gemfile +2 -0
- data/examples/sinatra/config.ru +2 -0
- data/examples/sinatra/sinatra_app.rb +6 -2
- data/exception_notification.gemspec +9 -8
- data/gemfiles/rails4_0.gemfile +3 -3
- data/gemfiles/rails4_1.gemfile +3 -3
- data/gemfiles/rails4_2.gemfile +3 -3
- data/gemfiles/rails5_0.gemfile +3 -3
- data/gemfiles/rails5_1.gemfile +3 -3
- data/gemfiles/rails5_2.gemfile +3 -3
- data/lib/exception_notification.rb +2 -0
- data/lib/exception_notification/rack.rb +24 -13
- data/lib/exception_notification/rails.rb +2 -0
- data/lib/exception_notification/resque.rb +2 -0
- data/lib/exception_notification/sidekiq.rb +5 -3
- data/lib/exception_notification/version.rb +3 -1
- data/lib/exception_notifier.rb +46 -7
- data/lib/exception_notifier/base_notifier.rb +8 -2
- data/lib/exception_notifier/campfire_notifier.rb +2 -0
- data/lib/exception_notifier/datadog_notifier.rb +12 -9
- data/lib/exception_notifier/email_notifier.rb +11 -3
- data/lib/exception_notifier/google_chat_notifier.rb +2 -0
- data/lib/exception_notifier/hipchat_notifier.rb +2 -0
- data/lib/exception_notifier/irc_notifier.rb +4 -3
- data/lib/exception_notifier/mattermost_notifier.rb +10 -0
- data/lib/exception_notifier/modules/backtrace_cleaner.rb +2 -0
- data/lib/exception_notifier/modules/error_grouping.rb +19 -9
- data/lib/exception_notifier/modules/formatter.rb +3 -0
- data/lib/exception_notifier/notifier.rb +5 -1
- data/lib/exception_notifier/slack_notifier.rb +2 -0
- data/lib/exception_notifier/sns_notifier.rb +4 -3
- data/lib/exception_notifier/teams_notifier.rb +10 -3
- data/lib/exception_notifier/webhook_notifier.rb +3 -3
- data/lib/generators/exception_notification/install_generator.rb +8 -2
- data/test/exception_notification/rack_test.rb +48 -2
- data/test/exception_notification/resque_test.rb +2 -0
- data/test/exception_notifier/campfire_notifier_test.rb +8 -1
- data/test/exception_notifier/datadog_notifier_test.rb +2 -0
- data/test/exception_notifier/email_notifier_test.rb +29 -3
- data/test/exception_notifier/google_chat_notifier_test.rb +15 -11
- data/test/exception_notifier/hipchat_notifier_test.rb +8 -2
- data/test/exception_notifier/irc_notifier_test.rb +2 -0
- data/test/exception_notifier/mattermost_notifier_test.rb +73 -24
- data/test/exception_notifier/modules/error_grouping_test.rb +2 -0
- data/test/exception_notifier/modules/formatter_test.rb +2 -0
- data/test/exception_notifier/sidekiq_test.rb +3 -11
- data/test/exception_notifier/slack_notifier_test.rb +12 -10
- data/test/exception_notifier/sns_notifier_test.rb +9 -7
- data/test/exception_notifier/teams_notifier_test.rb +2 -0
- data/test/exception_notifier/webhook_notifier_test.rb +6 -4
- data/test/exception_notifier_test.rb +112 -6
- data/test/support/exception_notifier_helper.rb +14 -0
- data/test/test_helper.rb +5 -1
- metadata +22 -27
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ExceptionNotifier
|
2
4
|
class BaseNotifier
|
3
5
|
attr_accessor :base_options
|
@@ -14,11 +16,15 @@ module ExceptionNotifier
|
|
14
16
|
end
|
15
17
|
|
16
18
|
def _pre_callback(exception, options, message, message_opts)
|
17
|
-
|
19
|
+
return unless @base_options[:pre_callback].respond_to?(:call)
|
20
|
+
|
21
|
+
@base_options[:pre_callback].call(options, self, exception.backtrace, message, message_opts)
|
18
22
|
end
|
19
23
|
|
20
24
|
def _post_callback(exception, options, message, message_opts)
|
21
|
-
|
25
|
+
return unless @base_options[:post_callback].respond_to?(:call)
|
26
|
+
|
27
|
+
@base_options[:post_callback].call(options, self, exception.backtrace, message, message_opts)
|
22
28
|
end
|
23
29
|
end
|
24
30
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'action_dispatch'
|
2
4
|
|
3
5
|
module ExceptionNotifier
|
@@ -30,7 +32,7 @@ module ExceptionNotifier
|
|
30
32
|
MAX_TITLE_LENGTH = 120
|
31
33
|
MAX_VALUE_LENGTH = 300
|
32
34
|
MAX_BACKTRACE_SIZE = 3
|
33
|
-
ALERT_TYPE = 'error'
|
35
|
+
ALERT_TYPE = 'error'
|
34
36
|
|
35
37
|
attr_reader :exception,
|
36
38
|
:options
|
@@ -74,11 +76,8 @@ module ExceptionNotifier
|
|
74
76
|
end
|
75
77
|
|
76
78
|
def formatted_title
|
77
|
-
title =
|
78
|
-
|
79
|
-
title << "#{controller.controller_name} #{controller.action_name}" if controller
|
80
|
-
title << " (#{exception.class})"
|
81
|
-
title << " #{exception.message.inspect}"
|
79
|
+
title =
|
80
|
+
"#{title_prefix}#{controller_subtitle} (#{exception.class}) #{exception.message.inspect}"
|
82
81
|
|
83
82
|
truncate(title, MAX_TITLE_LENGTH)
|
84
83
|
end
|
@@ -108,9 +107,7 @@ module ExceptionNotifier
|
|
108
107
|
text << formatted_key_value('Parameters', request.filtered_parameters.inspect)
|
109
108
|
text << formatted_key_value('Timestamp', Time.current)
|
110
109
|
text << formatted_key_value('Server', Socket.gethostname)
|
111
|
-
if defined?(Rails) && Rails.respond_to?(:root)
|
112
|
-
text << formatted_key_value('Rails root', Rails.root)
|
113
|
-
end
|
110
|
+
text << formatted_key_value('Rails root', Rails.root) if defined?(Rails) && Rails.respond_to?(:root)
|
114
111
|
text << formatted_key_value('Process', $PROCESS_ID)
|
115
112
|
text << '___'
|
116
113
|
text.join("\n")
|
@@ -148,6 +145,12 @@ module ExceptionNotifier
|
|
148
145
|
object.to_s
|
149
146
|
end
|
150
147
|
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def controller_subtitle
|
152
|
+
"#{controller.controller_name} #{controller.action_name}" if controller
|
153
|
+
end
|
151
154
|
end
|
152
155
|
end
|
153
156
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/core_ext/time'
|
2
4
|
require 'action_mailer'
|
3
5
|
require 'action_dispatch'
|
@@ -74,13 +76,17 @@ module ExceptionNotifier
|
|
74
76
|
def compose_subject
|
75
77
|
subject = @options[:email_prefix].to_s.dup
|
76
78
|
subject << "(#{@options[:accumulated_errors_count]} times)" if @options[:accumulated_errors_count].to_i > 1
|
77
|
-
subject << "#{@kontroller.controller_name} #{@kontroller.action_name}" if
|
79
|
+
subject << "#{@kontroller.controller_name} #{@kontroller.action_name}" if include_controller?
|
78
80
|
subject << " (#{@exception.class})"
|
79
81
|
subject << " #{@exception.message.inspect}" if @options[:verbose_subject]
|
80
82
|
subject = EmailNotifier.normalize_digits(subject) if @options[:normalize_subject]
|
81
83
|
subject.length > 120 ? subject[0...120] + '...' : subject
|
82
84
|
end
|
83
85
|
|
86
|
+
def include_controller?
|
87
|
+
@kontroller && @options[:include_controller_and_action_names_in_subject]
|
88
|
+
end
|
89
|
+
|
84
90
|
def set_data_variables
|
85
91
|
@data.each do |name, value|
|
86
92
|
instance_variable_set("@#{name}", value)
|
@@ -121,7 +127,7 @@ module ExceptionNotifier
|
|
121
127
|
headers = {
|
122
128
|
delivery_method: @options[:delivery_method],
|
123
129
|
to: exception_recipients,
|
124
|
-
from: @options[:sender_address],
|
130
|
+
from: self.class.default[:from] || @options[:sender_address],
|
125
131
|
subject: subject,
|
126
132
|
template_name: name
|
127
133
|
}.merge(@options[:email_headers])
|
@@ -137,7 +143,9 @@ module ExceptionNotifier
|
|
137
143
|
end
|
138
144
|
|
139
145
|
def load_custom_views
|
140
|
-
|
146
|
+
return unless defined?(Rails) && Rails.respond_to?(:root)
|
147
|
+
|
148
|
+
prepend_view_path Rails.root.nil? ? 'app/views' : "#{Rails.root}/app/views"
|
141
149
|
end
|
142
150
|
|
143
151
|
def maybe_call(maybe_proc)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ExceptionNotifier
|
2
4
|
class IrcNotifier < BaseNotifier
|
3
5
|
def initialize(options)
|
@@ -9,9 +11,8 @@ module ExceptionNotifier
|
|
9
11
|
def call(exception, options = {})
|
10
12
|
errors_count = options[:accumulated_errors_count].to_i
|
11
13
|
|
12
|
-
|
13
|
-
message
|
14
|
-
|
14
|
+
occurrences = "(#{errors_count} times)" if errors_count > 1
|
15
|
+
message = "#{occurrences}'#{exception.message}'"
|
15
16
|
message += " on '#{exception.backtrace.first}'" if exception.backtrace
|
16
17
|
|
17
18
|
return unless active?
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'httparty'
|
2
4
|
|
3
5
|
module ExceptionNotifier
|
@@ -10,6 +12,8 @@ module ExceptionNotifier
|
|
10
12
|
|
11
13
|
@gitlab_url = options[:git_url]
|
12
14
|
|
15
|
+
@env = options[:env] || {}
|
16
|
+
|
13
17
|
payload = {
|
14
18
|
text: message_text.compact.join("\n"),
|
15
19
|
username: options[:username] || 'Exception Notifier'
|
@@ -52,6 +56,12 @@ module ExceptionNotifier
|
|
52
56
|
text << backtrace
|
53
57
|
end
|
54
58
|
|
59
|
+
if (exception_data = @env['exception_notifier.exception_data'])
|
60
|
+
text << '### Data'
|
61
|
+
data_string = exception_data.map { |k, v| "* #{k} : #{v}" }.join("\n")
|
62
|
+
text << "```\n#{data_string}\n```"
|
63
|
+
end
|
64
|
+
|
55
65
|
text << message_issue_link if @gitlab_url
|
56
66
|
|
57
67
|
text
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/core_ext/numeric/time'
|
2
4
|
require 'active_support/concern'
|
3
5
|
|
@@ -25,20 +27,21 @@ module ExceptionNotifier
|
|
25
27
|
end
|
26
28
|
|
27
29
|
def error_count(error_key)
|
28
|
-
count =
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
count =
|
31
|
+
begin
|
32
|
+
error_grouping_cache.read(error_key)
|
33
|
+
rescue StandardError => e
|
34
|
+
log_cache_error(error_grouping_cache, e, :read)
|
35
|
+
fallback_cache_store.read(error_key)
|
36
|
+
end
|
34
37
|
|
35
|
-
count
|
38
|
+
count&.to_i
|
36
39
|
end
|
37
40
|
|
38
41
|
def save_error_count(error_key, count)
|
39
42
|
error_grouping_cache.write(error_key, count, expires_in: error_grouping_period)
|
40
43
|
rescue StandardError => e
|
41
|
-
|
44
|
+
log_cache_error(error_grouping_cache, e, :write)
|
42
45
|
fallback_cache_store.write(error_key, count, expires_in: error_grouping_period)
|
43
46
|
end
|
44
47
|
|
@@ -50,7 +53,8 @@ module ExceptionNotifier
|
|
50
53
|
accumulated_errors_count = count + 1
|
51
54
|
save_error_count(message_based_key, accumulated_errors_count)
|
52
55
|
else
|
53
|
-
backtrace_based_key =
|
56
|
+
backtrace_based_key =
|
57
|
+
"exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}"
|
54
58
|
|
55
59
|
if (count = error_grouping_cache.read(backtrace_based_key))
|
56
60
|
accumulated_errors_count = count + 1
|
@@ -72,6 +76,12 @@ module ExceptionNotifier
|
|
72
76
|
factor.to_i == factor
|
73
77
|
end
|
74
78
|
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def log_cache_error(cache, exception, action)
|
83
|
+
"#{cache.inspect} failed to #{action}, reason: #{exception.message}. Falling back to memory cache store."
|
84
|
+
end
|
75
85
|
end
|
76
86
|
end
|
77
87
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/core_ext/time'
|
2
4
|
require 'action_dispatch'
|
3
5
|
|
@@ -108,6 +110,7 @@ module ExceptionNotifier
|
|
108
110
|
|
109
111
|
def rails_app_name
|
110
112
|
return unless defined?(::Rails) && ::Rails.respond_to?(:application)
|
113
|
+
|
111
114
|
Rails.application.class.parent_name.underscore
|
112
115
|
end
|
113
116
|
|
@@ -1,9 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/deprecation'
|
2
4
|
|
3
5
|
module ExceptionNotifier
|
4
6
|
class Notifier
|
5
7
|
def self.exception_notification(env, exception, options = {})
|
6
|
-
ActiveSupport::Deprecation.warn
|
8
|
+
ActiveSupport::Deprecation.warn(
|
9
|
+
'Please use ExceptionNotifier.notify_exception(exception, options.merge(env: env)).'
|
10
|
+
)
|
7
11
|
ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options.merge(env: env))
|
8
12
|
end
|
9
13
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ExceptionNotifier
|
2
4
|
class SnsNotifier < BaseNotifier
|
3
5
|
def initialize(options)
|
@@ -33,9 +35,8 @@ module ExceptionNotifier
|
|
33
35
|
attr_reader :notifier, :options
|
34
36
|
|
35
37
|
def build_subject(exception, options)
|
36
|
-
subject =
|
37
|
-
|
38
|
-
subject << ' occurred'
|
38
|
+
subject =
|
39
|
+
"#{options[:sns_prefix]} - #{accumulated_exception_name(exception, options)} occurred"
|
39
40
|
subject.length > 120 ? subject[0...120] + '...' : subject
|
40
41
|
end
|
41
42
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'action_dispatch'
|
2
4
|
require 'active_support/core_ext/time'
|
3
5
|
require 'json'
|
@@ -64,8 +66,6 @@ module ExceptionNotifier
|
|
64
66
|
private
|
65
67
|
|
66
68
|
def message_text
|
67
|
-
errors_count = @options[:accumulated_errors_count].to_i
|
68
|
-
|
69
69
|
text = {
|
70
70
|
'@type' => 'MessageCard',
|
71
71
|
'@context' => 'http://schema.org/extensions',
|
@@ -73,7 +73,7 @@ module ExceptionNotifier
|
|
73
73
|
'title' => "⚠️ Exception Occurred in #{env_name} ⚠️",
|
74
74
|
'sections' => [
|
75
75
|
{
|
76
|
-
'activityTitle' =>
|
76
|
+
'activityTitle' => activity_title,
|
77
77
|
'activitySubtitle' => @exception.message.to_s
|
78
78
|
}
|
79
79
|
],
|
@@ -100,6 +100,13 @@ module ExceptionNotifier
|
|
100
100
|
details
|
101
101
|
end
|
102
102
|
|
103
|
+
def activity_title
|
104
|
+
errors_count = @options[:accumulated_errors_count].to_i
|
105
|
+
|
106
|
+
"#{errors_count > 1 ? errors_count : 'A'} *#{@exception.class}* occurred" +
|
107
|
+
(@controller ? " in *#{controller_and_method}*." : '.')
|
108
|
+
end
|
109
|
+
|
103
110
|
def message_request
|
104
111
|
{
|
105
112
|
'name' => 'Request',
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'action_dispatch'
|
2
4
|
require 'active_support/core_ext/time'
|
3
5
|
|
@@ -18,9 +20,7 @@ module ExceptionNotifier
|
|
18
20
|
options[:body] ||= {}
|
19
21
|
options[:body][:server] = Socket.gethostname
|
20
22
|
options[:body][:process] = $PROCESS_ID
|
21
|
-
if defined?(Rails) && Rails.respond_to?(:root)
|
22
|
-
options[:body][:rails_root] = Rails.root
|
23
|
-
end
|
23
|
+
options[:body][:rails_root] = Rails.root if defined?(Rails) && Rails.respond_to?(:root)
|
24
24
|
options[:body][:exception] = {
|
25
25
|
error_class: exception.class.to_s,
|
26
26
|
message: exception.message.inspect,
|
@@ -1,11 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ExceptionNotification
|
2
4
|
module Generators
|
3
5
|
class InstallGenerator < Rails::Generators::Base
|
4
6
|
desc 'Creates a ExceptionNotification initializer.'
|
5
7
|
|
6
8
|
source_root File.expand_path('templates', __dir__)
|
7
|
-
class_option :resque,
|
8
|
-
|
9
|
+
class_option :resque,
|
10
|
+
type: :boolean,
|
11
|
+
desc: 'Add support for sending notifications when errors occur in Resque jobs.'
|
12
|
+
class_option :sidekiq,
|
13
|
+
type: :boolean,
|
14
|
+
desc: 'Add support for sending notifications when errors occur in Sidekiq jobs.'
|
9
15
|
|
10
16
|
def copy_initializer
|
11
17
|
template 'exception_notification.rb.erb', 'config/initializers/exception_notification.rb'
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'test_helper'
|
2
4
|
|
3
5
|
class RackTest < ActiveSupport::TestCase
|
@@ -10,8 +12,7 @@ class RackTest < ActiveSupport::TestCase
|
|
10
12
|
end
|
11
13
|
|
12
14
|
teardown do
|
13
|
-
ExceptionNotifier.
|
14
|
-
ExceptionNotifier.notification_trigger = nil
|
15
|
+
ExceptionNotifier.reset_notifiers!
|
15
16
|
end
|
16
17
|
|
17
18
|
test 'should ignore "X-Cascade" header by default' do
|
@@ -57,4 +58,49 @@ class RackTest < ActiveSupport::TestCase
|
|
57
58
|
refute env['exception_notifier.delivered']
|
58
59
|
end
|
59
60
|
end
|
61
|
+
|
62
|
+
test 'should ignore exceptions if ignore_if condition is met' do
|
63
|
+
exception_app = Object.new
|
64
|
+
exception_app.stubs(:call).raises(RuntimeError)
|
65
|
+
|
66
|
+
env = {}
|
67
|
+
|
68
|
+
begin
|
69
|
+
ExceptionNotification::Rack.new(
|
70
|
+
exception_app,
|
71
|
+
ignore_if: ->(_env, exception) { exception.is_a? RuntimeError }
|
72
|
+
).call(env)
|
73
|
+
|
74
|
+
flunk
|
75
|
+
rescue StandardError
|
76
|
+
refute env['exception_notifier.delivered']
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
test 'should ignore exceptions with notifiers that satisfies ignore_notifier_if condition' do
|
81
|
+
exception_app = Object.new
|
82
|
+
exception_app.stubs(:call).raises(RuntimeError)
|
83
|
+
|
84
|
+
notifier1_called = notifier2_called = false
|
85
|
+
notifier1 = ->(_exception, _options) { notifier1_called = true }
|
86
|
+
notifier2 = ->(_exception, _options) { notifier2_called = true }
|
87
|
+
|
88
|
+
env = {}
|
89
|
+
|
90
|
+
begin
|
91
|
+
ExceptionNotification::Rack.new(
|
92
|
+
exception_app,
|
93
|
+
ignore_notifier_if: {
|
94
|
+
notifier1: ->(_env, exception) { exception.is_a? RuntimeError }
|
95
|
+
},
|
96
|
+
notifier1: notifier1,
|
97
|
+
notifier2: notifier2
|
98
|
+
).call(env)
|
99
|
+
|
100
|
+
flunk
|
101
|
+
rescue StandardError
|
102
|
+
refute notifier1_called
|
103
|
+
assert notifier2_called
|
104
|
+
end
|
105
|
+
end
|
60
106
|
end
|