exception_notification 4.2.1 → 4.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/Appraisals +4 -3
- data/CHANGELOG.rdoc +57 -1
- data/CONTRIBUTING.md +21 -2
- data/Gemfile +3 -1
- data/README.md +105 -780
- data/Rakefile +4 -2
- data/docs/notifiers/campfire.md +50 -0
- data/docs/notifiers/custom.md +42 -0
- data/docs/notifiers/datadog.md +51 -0
- data/docs/notifiers/email.md +195 -0
- data/docs/notifiers/google_chat.md +31 -0
- data/docs/notifiers/hipchat.md +66 -0
- data/docs/notifiers/irc.md +97 -0
- data/docs/notifiers/mattermost.md +115 -0
- data/docs/notifiers/slack.md +161 -0
- data/docs/notifiers/sns.md +37 -0
- data/docs/notifiers/teams.md +54 -0
- data/docs/notifiers/webhook.md +60 -0
- data/examples/sample_app.rb +56 -0
- data/examples/sinatra/Gemfile +8 -6
- data/examples/sinatra/config.ru +3 -1
- data/examples/sinatra/sinatra_app.rb +19 -11
- data/exception_notification.gemspec +30 -23
- data/gemfiles/rails4_0.gemfile +1 -2
- data/gemfiles/rails4_1.gemfile +1 -2
- data/gemfiles/rails4_2.gemfile +1 -2
- data/gemfiles/rails5_0.gemfile +1 -2
- data/gemfiles/rails5_1.gemfile +7 -0
- data/gemfiles/rails5_2.gemfile +7 -0
- data/gemfiles/rails6_0.gemfile +7 -0
- data/lib/exception_notification.rb +3 -0
- data/lib/exception_notification/rack.rb +30 -23
- data/lib/exception_notification/rails.rb +3 -0
- data/lib/exception_notification/resque.rb +10 -10
- data/lib/exception_notification/sidekiq.rb +10 -12
- data/lib/exception_notification/version.rb +5 -0
- data/lib/exception_notifier.rb +79 -11
- data/lib/exception_notifier/base_notifier.rb +10 -5
- data/lib/exception_notifier/campfire_notifier.rb +14 -9
- data/lib/exception_notifier/datadog_notifier.rb +156 -0
- data/lib/exception_notifier/email_notifier.rb +78 -87
- data/lib/exception_notifier/google_chat_notifier.rb +44 -0
- data/lib/exception_notifier/hipchat_notifier.rb +16 -10
- data/lib/exception_notifier/irc_notifier.rb +38 -31
- data/lib/exception_notifier/mattermost_notifier.rb +54 -131
- data/lib/exception_notifier/modules/backtrace_cleaner.rb +2 -2
- data/lib/exception_notifier/modules/error_grouping.rb +87 -0
- data/lib/exception_notifier/modules/formatter.rb +121 -0
- data/lib/exception_notifier/notifier.rb +9 -6
- data/lib/exception_notifier/slack_notifier.rb +71 -40
- data/lib/exception_notifier/sns_notifier.rb +86 -0
- data/lib/exception_notifier/teams_notifier.rb +200 -0
- data/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +1 -1
- data/lib/exception_notifier/views/exception_notifier/_environment.text.erb +1 -1
- data/lib/exception_notifier/views/exception_notifier/_request.text.erb +1 -1
- data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +9 -9
- data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +2 -4
- data/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +2 -2
- data/lib/exception_notifier/webhook_notifier.rb +17 -14
- data/lib/generators/exception_notification/install_generator.rb +11 -5
- data/lib/generators/exception_notification/templates/{exception_notification.rb → exception_notification.rb.erb} +13 -11
- data/test/exception_notification/rack_test.rb +90 -4
- data/test/exception_notification/resque_test.rb +54 -0
- data/test/exception_notifier/campfire_notifier_test.rb +59 -38
- data/test/exception_notifier/datadog_notifier_test.rb +153 -0
- data/test/exception_notifier/email_notifier_test.rb +279 -145
- data/test/exception_notifier/google_chat_notifier_test.rb +185 -0
- data/test/exception_notifier/hipchat_notifier_test.rb +105 -64
- data/test/exception_notifier/irc_notifier_test.rb +48 -30
- data/test/exception_notifier/mattermost_notifier_test.rb +218 -55
- data/test/exception_notifier/modules/error_grouping_test.rb +167 -0
- data/test/exception_notifier/modules/formatter_test.rb +152 -0
- data/test/exception_notifier/sidekiq_test.rb +9 -17
- data/test/exception_notifier/slack_notifier_test.rb +84 -62
- data/test/exception_notifier/sns_notifier_test.rb +123 -0
- data/test/exception_notifier/teams_notifier_test.rb +92 -0
- data/test/exception_notifier/webhook_notifier_test.rb +52 -48
- data/test/exception_notifier_test.rb +220 -37
- data/test/support/exception_notifier_helper.rb +14 -0
- data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.html.erb +0 -0
- data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.text.erb +0 -0
- data/test/{dummy/app → support}/views/exception_notifier/_new_section.html.erb +0 -0
- data/test/{dummy/app → support}/views/exception_notifier/_new_section.text.erb +0 -0
- data/test/test_helper.rb +14 -13
- metadata +154 -162
- data/test/dummy/.gitignore +0 -4
- data/test/dummy/Rakefile +0 -7
- data/test/dummy/app/controllers/application_controller.rb +0 -3
- data/test/dummy/app/controllers/posts_controller.rb +0 -30
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/helpers/posts_helper.rb +0 -2
- data/test/dummy/app/models/post.rb +0 -2
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/app/views/posts/_form.html.erb +0 -0
- data/test/dummy/app/views/posts/new.html.erb +0 -0
- data/test/dummy/app/views/posts/show.html.erb +0 -0
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -42
- data/test/dummy/config/boot.rb +0 -6
- data/test/dummy/config/database.yml +0 -22
- data/test/dummy/config/environment.rb +0 -17
- data/test/dummy/config/environments/development.rb +0 -25
- data/test/dummy/config/environments/production.rb +0 -50
- data/test/dummy/config/environments/test.rb +0 -35
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/inflections.rb +0 -10
- data/test/dummy/config/initializers/mime_types.rb +0 -5
- data/test/dummy/config/initializers/secret_token.rb +0 -8
- data/test/dummy/config/initializers/session_store.rb +0 -8
- data/test/dummy/config/locales/en.yml +0 -5
- data/test/dummy/config/routes.rb +0 -3
- data/test/dummy/db/migrate/20110729022608_create_posts.rb +0 -15
- data/test/dummy/db/schema.rb +0 -24
- data/test/dummy/db/seeds.rb +0 -7
- data/test/dummy/lib/tasks/.gitkeep +0 -0
- data/test/dummy/public/404.html +0 -26
- data/test/dummy/public/422.html +0 -26
- data/test/dummy/public/500.html +0 -26
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/images/rails.png +0 -0
- data/test/dummy/public/index.html +0 -239
- data/test/dummy/public/javascripts/application.js +0 -2
- data/test/dummy/public/javascripts/controls.js +0 -965
- data/test/dummy/public/javascripts/dragdrop.js +0 -974
- data/test/dummy/public/javascripts/effects.js +0 -1123
- data/test/dummy/public/javascripts/prototype.js +0 -6001
- data/test/dummy/public/javascripts/rails.js +0 -191
- data/test/dummy/public/robots.txt +0 -5
- data/test/dummy/public/stylesheets/.gitkeep +0 -0
- data/test/dummy/public/stylesheets/scaffold.css +0 -56
- data/test/dummy/script/rails +0 -6
- data/test/dummy/test/functional/posts_controller_test.rb +0 -218
- data/test/dummy/test/test_helper.rb +0 -7
|
@@ -1,18 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'sidekiq'
|
|
2
4
|
|
|
3
5
|
# Note: this class is only needed for Sidekiq version < 3.
|
|
4
6
|
module ExceptionNotification
|
|
5
7
|
class Sidekiq
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
ExceptionNotifier.notify_exception(exception, :data => { :sidekiq => msg })
|
|
12
|
-
raise exception
|
|
13
|
-
end
|
|
8
|
+
def call(_worker, msg, _queue)
|
|
9
|
+
yield
|
|
10
|
+
rescue Exception => e
|
|
11
|
+
ExceptionNotifier.notify_exception(e, data: { sidekiq: msg })
|
|
12
|
+
raise e
|
|
14
13
|
end
|
|
15
|
-
|
|
16
14
|
end
|
|
17
15
|
end
|
|
18
16
|
|
|
@@ -24,8 +22,8 @@ if ::Sidekiq::VERSION < '3'
|
|
|
24
22
|
end
|
|
25
23
|
else
|
|
26
24
|
::Sidekiq.configure_server do |config|
|
|
27
|
-
config.error_handlers <<
|
|
28
|
-
ExceptionNotifier.notify_exception(ex, :
|
|
29
|
-
|
|
25
|
+
config.error_handlers << proc do |ex, context|
|
|
26
|
+
ExceptionNotifier.notify_exception(ex, data: { sidekiq: context })
|
|
27
|
+
end
|
|
30
28
|
end
|
|
31
29
|
end
|
data/lib/exception_notifier.rb
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'logger'
|
|
2
4
|
require 'active_support/core_ext/string/inflections'
|
|
3
5
|
require 'active_support/core_ext/module/attribute_accessors'
|
|
4
6
|
require 'exception_notifier/base_notifier'
|
|
7
|
+
require 'exception_notifier/modules/error_grouping'
|
|
5
8
|
|
|
6
9
|
module ExceptionNotifier
|
|
10
|
+
include ErrorGrouping
|
|
7
11
|
|
|
8
12
|
autoload :BacktraceCleaner, 'exception_notifier/modules/backtrace_cleaner'
|
|
13
|
+
autoload :Formatter, 'exception_notifier/modules/formatter'
|
|
9
14
|
|
|
10
15
|
autoload :Notifier, 'exception_notifier/notifier'
|
|
11
16
|
autoload :EmailNotifier, 'exception_notifier/email_notifier'
|
|
@@ -15,6 +20,10 @@ module ExceptionNotifier
|
|
|
15
20
|
autoload :IrcNotifier, 'exception_notifier/irc_notifier'
|
|
16
21
|
autoload :SlackNotifier, 'exception_notifier/slack_notifier'
|
|
17
22
|
autoload :MattermostNotifier, 'exception_notifier/mattermost_notifier'
|
|
23
|
+
autoload :TeamsNotifier, 'exception_notifier/teams_notifier'
|
|
24
|
+
autoload :SnsNotifier, 'exception_notifier/sns_notifier'
|
|
25
|
+
autoload :GoogleChatNotifier, 'exception_notifier/google_chat_notifier'
|
|
26
|
+
autoload :DatadogNotifier, 'exception_notifier/datadog_notifier'
|
|
18
27
|
|
|
19
28
|
class UndefinedNotifierError < StandardError; end
|
|
20
29
|
|
|
@@ -24,7 +33,10 @@ module ExceptionNotifier
|
|
|
24
33
|
|
|
25
34
|
# Define a set of exceptions to be ignored, ie, dont send notifications when any of them are raised.
|
|
26
35
|
mattr_accessor :ignored_exceptions
|
|
27
|
-
@@ignored_exceptions = %w
|
|
36
|
+
@@ignored_exceptions = %w[
|
|
37
|
+
ActiveRecord::RecordNotFound Mongoid::Errors::DocumentNotFound AbstractController::ActionNotFound
|
|
38
|
+
ActionController::RoutingError ActionController::UnknownFormat ActionController::UrlGenerationError
|
|
39
|
+
]
|
|
28
40
|
|
|
29
41
|
mattr_accessor :testing_mode
|
|
30
42
|
@@testing_mode = false
|
|
@@ -33,6 +45,9 @@ module ExceptionNotifier
|
|
|
33
45
|
# Store conditions that decide when exceptions must be ignored or not.
|
|
34
46
|
@@ignores = []
|
|
35
47
|
|
|
48
|
+
# Store by-notifier conditions that decide when exceptions must be ignored or not.
|
|
49
|
+
@@by_notifier_ignores = {}
|
|
50
|
+
|
|
36
51
|
# Store notifiers that send notifications when exceptions are raised.
|
|
37
52
|
@@notifiers = {}
|
|
38
53
|
|
|
@@ -40,14 +55,25 @@ module ExceptionNotifier
|
|
|
40
55
|
self.testing_mode = true
|
|
41
56
|
end
|
|
42
57
|
|
|
43
|
-
def notify_exception(exception, options={})
|
|
58
|
+
def notify_exception(exception, options = {}, &block)
|
|
44
59
|
return false if ignored_exception?(options[:ignore_exceptions], exception)
|
|
45
60
|
return false if ignored?(exception, options)
|
|
61
|
+
|
|
62
|
+
if error_grouping
|
|
63
|
+
errors_count = group_error!(exception, options)
|
|
64
|
+
return false unless send_notification?(exception, errors_count)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
notification_fired = false
|
|
46
68
|
selected_notifiers = options.delete(:notifiers) || notifiers
|
|
47
69
|
[*selected_notifiers].each do |notifier|
|
|
48
|
-
|
|
70
|
+
unless notifier_ignored?(exception, options, notifier: notifier)
|
|
71
|
+
fire_notification(notifier, exception, options.dup, &block)
|
|
72
|
+
notification_fired = true
|
|
73
|
+
end
|
|
49
74
|
end
|
|
50
|
-
|
|
75
|
+
|
|
76
|
+
notification_fired
|
|
51
77
|
end
|
|
52
78
|
|
|
53
79
|
def register_exception_notifier(name, notifier_or_options)
|
|
@@ -82,31 +108,65 @@ module ExceptionNotifier
|
|
|
82
108
|
@@ignores << block
|
|
83
109
|
end
|
|
84
110
|
|
|
111
|
+
def ignore_notifier_if(notifier, &block)
|
|
112
|
+
@@by_notifier_ignores[notifier] = block
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def ignore_crawlers(crawlers)
|
|
116
|
+
ignore_if do |_exception, opts|
|
|
117
|
+
opts.key?(:env) && from_crawler(opts[:env], crawlers)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
85
121
|
def clear_ignore_conditions!
|
|
86
122
|
@@ignores.clear
|
|
123
|
+
@@by_notifier_ignores.clear
|
|
87
124
|
end
|
|
88
125
|
|
|
89
126
|
private
|
|
127
|
+
|
|
90
128
|
def ignored?(exception, options)
|
|
91
|
-
@@ignores.any?{ |condition| condition.call(exception, options) }
|
|
129
|
+
@@ignores.any? { |condition| condition.call(exception, options) }
|
|
92
130
|
rescue Exception => e
|
|
93
131
|
raise e if @@testing_mode
|
|
94
132
|
|
|
95
|
-
logger.warn
|
|
133
|
+
logger.warn(
|
|
134
|
+
"An error occurred when evaluating an ignore condition. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
|
135
|
+
)
|
|
136
|
+
false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def notifier_ignored?(exception, options, notifier:)
|
|
140
|
+
return false unless @@by_notifier_ignores.key?(notifier)
|
|
141
|
+
|
|
142
|
+
condition = @@by_notifier_ignores[notifier]
|
|
143
|
+
condition.call(exception, options)
|
|
144
|
+
rescue Exception => e
|
|
145
|
+
raise e if @@testing_mode
|
|
146
|
+
|
|
147
|
+
logger.warn(<<~"MESSAGE")
|
|
148
|
+
An error occurred when evaluating a by-notifier ignore condition. #{e.class}: #{e.message}
|
|
149
|
+
#{e.backtrace.join("\n")}
|
|
150
|
+
MESSAGE
|
|
96
151
|
false
|
|
97
152
|
end
|
|
98
153
|
|
|
99
154
|
def ignored_exception?(ignore_array, exception)
|
|
100
|
-
(Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s)
|
|
155
|
+
all_ignored_exceptions = (Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s)
|
|
156
|
+
exception_ancestors = exception.singleton_class.ancestors.map(&:to_s)
|
|
157
|
+
!(all_ignored_exceptions & exception_ancestors).empty?
|
|
101
158
|
end
|
|
102
159
|
|
|
103
|
-
def fire_notification(notifier_name, exception, options)
|
|
160
|
+
def fire_notification(notifier_name, exception, options, &block)
|
|
104
161
|
notifier = registered_exception_notifier(notifier_name)
|
|
105
|
-
notifier.call(exception, options)
|
|
162
|
+
notifier.call(exception, options, &block)
|
|
106
163
|
rescue Exception => e
|
|
107
164
|
raise e if @@testing_mode
|
|
108
165
|
|
|
109
|
-
logger.warn
|
|
166
|
+
logger.warn(
|
|
167
|
+
"An error occurred when sending a notification using '#{notifier_name}' notifier." \
|
|
168
|
+
"#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
|
169
|
+
)
|
|
110
170
|
false
|
|
111
171
|
end
|
|
112
172
|
|
|
@@ -116,7 +176,15 @@ module ExceptionNotifier
|
|
|
116
176
|
notifier = notifier_class.new(options)
|
|
117
177
|
register_exception_notifier(name, notifier)
|
|
118
178
|
rescue NameError => e
|
|
119
|
-
raise UndefinedNotifierError,
|
|
179
|
+
raise UndefinedNotifierError,
|
|
180
|
+
"No notifier named '#{name}' was found. Please, revise your configuration options. Cause: #{e.message}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def from_crawler(env, ignored_crawlers)
|
|
184
|
+
agent = env['HTTP_USER_AGENT']
|
|
185
|
+
Array(ignored_crawlers).any? do |crawler|
|
|
186
|
+
agent =~ Regexp.new(crawler)
|
|
187
|
+
end
|
|
120
188
|
end
|
|
121
189
|
end
|
|
122
190
|
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ExceptionNotifier
|
|
2
4
|
class BaseNotifier
|
|
3
5
|
attr_accessor :base_options
|
|
4
6
|
|
|
5
|
-
def initialize(options={})
|
|
7
|
+
def initialize(options = {})
|
|
6
8
|
@base_options = options
|
|
7
9
|
end
|
|
8
10
|
|
|
9
|
-
def send_notice(exception, options, message, message_opts=nil)
|
|
11
|
+
def send_notice(exception, options, message, message_opts = nil)
|
|
10
12
|
_pre_callback(exception, options, message, message_opts)
|
|
11
13
|
result = yield(message, message_opts)
|
|
12
14
|
_post_callback(exception, options, message, message_opts)
|
|
@@ -14,12 +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
|
-
|
|
22
|
-
end
|
|
25
|
+
return unless @base_options[:post_callback].respond_to?(:call)
|
|
23
26
|
|
|
27
|
+
@base_options[:post_callback].call(options, self, exception.backtrace, message, message_opts)
|
|
28
|
+
end
|
|
24
29
|
end
|
|
25
30
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ExceptionNotifier
|
|
2
4
|
class CampfireNotifier < BaseNotifier
|
|
3
|
-
|
|
4
5
|
attr_accessor :subdomain
|
|
5
6
|
attr_accessor :token
|
|
6
7
|
attr_accessor :room
|
|
@@ -12,18 +13,22 @@ module ExceptionNotifier
|
|
|
12
13
|
room_name = options.delete(:room_name)
|
|
13
14
|
@campfire = Tinder::Campfire.new subdomain, options
|
|
14
15
|
@room = @campfire.find_room_by_name room_name
|
|
15
|
-
rescue
|
|
16
|
+
rescue StandardError
|
|
16
17
|
@campfire = @room = nil
|
|
17
18
|
end
|
|
18
19
|
end
|
|
19
20
|
|
|
20
|
-
def call(exception, options={})
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
def call(exception, options = {})
|
|
22
|
+
return unless active?
|
|
23
|
+
|
|
24
|
+
message = if options[:accumulated_errors_count].to_i > 1
|
|
25
|
+
"The exception occurred #{options[:accumulated_errors_count]} times: '#{exception.message}'"
|
|
26
|
+
else
|
|
27
|
+
"A new exception occurred: '#{exception.message}'"
|
|
28
|
+
end
|
|
29
|
+
message += " on '#{exception.backtrace.first}'" if exception.backtrace
|
|
30
|
+
send_notice(exception, options, message) do |msg, _|
|
|
31
|
+
@room.paste msg
|
|
27
32
|
end
|
|
28
33
|
end
|
|
29
34
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'action_dispatch'
|
|
4
|
+
|
|
5
|
+
module ExceptionNotifier
|
|
6
|
+
class DatadogNotifier < BaseNotifier
|
|
7
|
+
attr_reader :client,
|
|
8
|
+
:default_options
|
|
9
|
+
|
|
10
|
+
def initialize(options)
|
|
11
|
+
super
|
|
12
|
+
@client = options.fetch(:client)
|
|
13
|
+
@default_options = options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(exception, options = {})
|
|
17
|
+
client.emit_event(
|
|
18
|
+
datadog_event(exception, options)
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def datadog_event(exception, options = {})
|
|
23
|
+
DatadogExceptionEvent.new(
|
|
24
|
+
exception,
|
|
25
|
+
options.reverse_merge(default_options)
|
|
26
|
+
).event
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class DatadogExceptionEvent
|
|
30
|
+
include ExceptionNotifier::BacktraceCleaner
|
|
31
|
+
|
|
32
|
+
MAX_TITLE_LENGTH = 120
|
|
33
|
+
MAX_VALUE_LENGTH = 300
|
|
34
|
+
MAX_BACKTRACE_SIZE = 3
|
|
35
|
+
ALERT_TYPE = 'error'
|
|
36
|
+
|
|
37
|
+
attr_reader :exception,
|
|
38
|
+
:options
|
|
39
|
+
|
|
40
|
+
def initialize(exception, options)
|
|
41
|
+
@exception = exception
|
|
42
|
+
@options = options
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def request
|
|
46
|
+
@request ||= ActionDispatch::Request.new(options[:env]) if options[:env]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def controller
|
|
50
|
+
@controller ||= options[:env] && options[:env]['action_controller.instance']
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def backtrace
|
|
54
|
+
@backtrace ||= exception.backtrace ? clean_backtrace(exception) : []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def tags
|
|
58
|
+
options[:tags] || []
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def title_prefix
|
|
62
|
+
options[:title_prefix] || ''
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def event
|
|
66
|
+
title = formatted_title
|
|
67
|
+
body = formatted_body
|
|
68
|
+
|
|
69
|
+
Dogapi::Event.new(
|
|
70
|
+
body,
|
|
71
|
+
msg_title: title,
|
|
72
|
+
alert_type: ALERT_TYPE,
|
|
73
|
+
tags: tags,
|
|
74
|
+
aggregation_key: [title]
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def formatted_title
|
|
79
|
+
title =
|
|
80
|
+
"#{title_prefix}#{controller_subtitle} (#{exception.class}) #{exception.message.inspect}"
|
|
81
|
+
|
|
82
|
+
truncate(title, MAX_TITLE_LENGTH)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def formatted_body
|
|
86
|
+
text = []
|
|
87
|
+
|
|
88
|
+
text << '%%%'
|
|
89
|
+
text << formatted_request if request
|
|
90
|
+
text << formatted_session if request
|
|
91
|
+
text << formatted_backtrace
|
|
92
|
+
text << '%%%'
|
|
93
|
+
|
|
94
|
+
text.join("\n")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def formatted_key_value(key, value)
|
|
98
|
+
"**#{key}:** #{value}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def formatted_request
|
|
102
|
+
text = []
|
|
103
|
+
text << '### **Request**'
|
|
104
|
+
text << formatted_key_value('URL', request.url)
|
|
105
|
+
text << formatted_key_value('HTTP Method', request.request_method)
|
|
106
|
+
text << formatted_key_value('IP Address', request.remote_ip)
|
|
107
|
+
text << formatted_key_value('Parameters', request.filtered_parameters.inspect)
|
|
108
|
+
text << formatted_key_value('Timestamp', Time.current)
|
|
109
|
+
text << formatted_key_value('Server', Socket.gethostname)
|
|
110
|
+
text << formatted_key_value('Rails root', Rails.root) if defined?(Rails) && Rails.respond_to?(:root)
|
|
111
|
+
text << formatted_key_value('Process', $PROCESS_ID)
|
|
112
|
+
text << '___'
|
|
113
|
+
text.join("\n")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def formatted_session
|
|
117
|
+
text = []
|
|
118
|
+
text << '### **Session**'
|
|
119
|
+
text << formatted_key_value('Data', request.session.to_hash)
|
|
120
|
+
text << '___'
|
|
121
|
+
text.join("\n")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def formatted_backtrace
|
|
125
|
+
size = [backtrace.size, MAX_BACKTRACE_SIZE].min
|
|
126
|
+
|
|
127
|
+
text = []
|
|
128
|
+
text << '### **Backtrace**'
|
|
129
|
+
text << '````'
|
|
130
|
+
size.times { |i| text << backtrace[i] }
|
|
131
|
+
text << '````'
|
|
132
|
+
text << '___'
|
|
133
|
+
text.join("\n")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def truncate(string, max)
|
|
137
|
+
string.length > max ? "#{string[0...max]}..." : string
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def inspect_object(object)
|
|
141
|
+
case object
|
|
142
|
+
when Hash, Array
|
|
143
|
+
truncate(object.inspect, MAX_VALUE_LENGTH)
|
|
144
|
+
else
|
|
145
|
+
object.to_s
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def controller_subtitle
|
|
152
|
+
"#{controller.controller_name} #{controller.action_name}" if controller
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'active_support/core_ext/time'
|
|
3
4
|
require 'action_mailer'
|
|
4
5
|
require 'action_dispatch'
|
|
@@ -6,47 +7,61 @@ require 'pp'
|
|
|
6
7
|
|
|
7
8
|
module ExceptionNotifier
|
|
8
9
|
class EmailNotifier < BaseNotifier
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
DEFAULT_OPTIONS = {
|
|
11
|
+
sender_address: %("Exception Notifier" <exception.notifier@example.com>),
|
|
12
|
+
exception_recipients: [],
|
|
13
|
+
email_prefix: '[ERROR] ',
|
|
14
|
+
email_format: :text,
|
|
15
|
+
sections: %w[request session environment backtrace],
|
|
16
|
+
background_sections: %w[backtrace data],
|
|
17
|
+
verbose_subject: true,
|
|
18
|
+
normalize_subject: false,
|
|
19
|
+
include_controller_and_action_names_in_subject: true,
|
|
20
|
+
delivery_method: nil,
|
|
21
|
+
mailer_settings: nil,
|
|
22
|
+
email_headers: {},
|
|
23
|
+
mailer_parent: 'ActionMailer::Base',
|
|
24
|
+
template_path: 'exception_notifier',
|
|
25
|
+
deliver_with: nil
|
|
26
|
+
}.freeze
|
|
14
27
|
|
|
15
28
|
module Mailer
|
|
16
29
|
class MissingController
|
|
17
|
-
def method_missing(*args, &block)
|
|
18
|
-
end
|
|
30
|
+
def method_missing(*args, &block); end
|
|
19
31
|
end
|
|
20
32
|
|
|
21
33
|
def self.extended(base)
|
|
22
34
|
base.class_eval do
|
|
23
|
-
|
|
35
|
+
send(:include, ExceptionNotifier::BacktraceCleaner)
|
|
24
36
|
|
|
25
37
|
# Append application view path to the ExceptionNotifier lookup context.
|
|
26
|
-
|
|
38
|
+
append_view_path "#{File.dirname(__FILE__)}/views"
|
|
27
39
|
|
|
28
|
-
def exception_notification(env, exception, options={}, default_options={})
|
|
40
|
+
def exception_notification(env, exception, options = {}, default_options = {})
|
|
29
41
|
load_custom_views
|
|
30
42
|
|
|
31
43
|
@env = env
|
|
32
44
|
@exception = exception
|
|
33
|
-
|
|
45
|
+
|
|
46
|
+
env_options = env['exception_notifier.options'] || {}
|
|
47
|
+
@options = default_options.merge(env_options).merge(options)
|
|
48
|
+
|
|
34
49
|
@kontroller = env['action_controller.instance'] || MissingController.new
|
|
35
50
|
@request = ActionDispatch::Request.new(env)
|
|
36
51
|
@backtrace = exception.backtrace ? clean_backtrace(exception) : []
|
|
37
52
|
@timestamp = Time.current
|
|
38
53
|
@sections = @options[:sections]
|
|
39
54
|
@data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
|
|
40
|
-
@sections
|
|
55
|
+
@sections += %w[data] unless @data.empty?
|
|
41
56
|
|
|
42
57
|
compose_email
|
|
43
58
|
end
|
|
44
59
|
|
|
45
|
-
def background_exception_notification(exception, options={}, default_options={})
|
|
60
|
+
def background_exception_notification(exception, options = {}, default_options = {})
|
|
46
61
|
load_custom_views
|
|
47
62
|
|
|
48
63
|
@exception = exception
|
|
49
|
-
@options =
|
|
64
|
+
@options = default_options.merge(options).symbolize_keys
|
|
50
65
|
@backtrace = exception.backtrace || []
|
|
51
66
|
@timestamp = Time.current
|
|
52
67
|
@sections = @options[:background_sections]
|
|
@@ -59,12 +74,17 @@ module ExceptionNotifier
|
|
|
59
74
|
private
|
|
60
75
|
|
|
61
76
|
def compose_subject
|
|
62
|
-
subject =
|
|
63
|
-
subject << "#{@
|
|
77
|
+
subject = @options[:email_prefix].to_s.dup
|
|
78
|
+
subject << "(#{@options[:accumulated_errors_count]} times)" if @options[:accumulated_errors_count].to_i > 1
|
|
79
|
+
subject << "#{@kontroller.controller_name} #{@kontroller.action_name}" if include_controller?
|
|
64
80
|
subject << " (#{@exception.class})"
|
|
65
81
|
subject << " #{@exception.message.inspect}" if @options[:verbose_subject]
|
|
66
82
|
subject = EmailNotifier.normalize_digits(subject) if @options[:normalize_subject]
|
|
67
|
-
subject.length > 120 ? subject[0...120] +
|
|
83
|
+
subject.length > 120 ? subject[0...120] + '...' : subject
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def include_controller?
|
|
87
|
+
@kontroller && @options[:include_controller_and_action_names_in_subject]
|
|
68
88
|
end
|
|
69
89
|
|
|
70
90
|
def set_data_variables
|
|
@@ -75,19 +95,23 @@ module ExceptionNotifier
|
|
|
75
95
|
|
|
76
96
|
helper_method :inspect_object
|
|
77
97
|
|
|
98
|
+
def truncate(string, max)
|
|
99
|
+
string.length > max ? "#{string[0...max]}..." : string
|
|
100
|
+
end
|
|
101
|
+
|
|
78
102
|
def inspect_object(object)
|
|
79
103
|
case object
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
when Hash, Array
|
|
105
|
+
truncate(object.inspect, 300)
|
|
106
|
+
else
|
|
107
|
+
object.to_s
|
|
84
108
|
end
|
|
85
109
|
end
|
|
86
110
|
|
|
87
111
|
helper_method :safe_encode
|
|
88
112
|
|
|
89
113
|
def safe_encode(value)
|
|
90
|
-
value.encode(
|
|
114
|
+
value.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
|
|
91
115
|
end
|
|
92
116
|
|
|
93
117
|
def html_mail?
|
|
@@ -101,11 +125,11 @@ module ExceptionNotifier
|
|
|
101
125
|
exception_recipients = maybe_call(@options[:exception_recipients])
|
|
102
126
|
|
|
103
127
|
headers = {
|
|
104
|
-
:
|
|
105
|
-
:
|
|
106
|
-
:
|
|
107
|
-
:
|
|
108
|
-
:
|
|
128
|
+
delivery_method: @options[:delivery_method],
|
|
129
|
+
to: exception_recipients,
|
|
130
|
+
from: @options[:sender_address],
|
|
131
|
+
subject: subject,
|
|
132
|
+
template_name: name
|
|
109
133
|
}.merge(@options[:email_headers])
|
|
110
134
|
|
|
111
135
|
mail = mail(headers) do |format|
|
|
@@ -119,9 +143,9 @@ module ExceptionNotifier
|
|
|
119
143
|
end
|
|
120
144
|
|
|
121
145
|
def load_custom_views
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
146
|
+
return unless defined?(Rails) && Rails.respond_to?(:root)
|
|
147
|
+
|
|
148
|
+
prepend_view_path Rails.root.nil? ? 'app/views' : "#{Rails.root}/app/views"
|
|
125
149
|
end
|
|
126
150
|
|
|
127
151
|
def maybe_call(maybe_proc)
|
|
@@ -133,81 +157,48 @@ module ExceptionNotifier
|
|
|
133
157
|
|
|
134
158
|
def initialize(options)
|
|
135
159
|
super
|
|
160
|
+
|
|
136
161
|
delivery_method = (options[:delivery_method] || :smtp)
|
|
137
162
|
mailer_settings_key = "#{delivery_method}_settings".to_sym
|
|
138
163
|
options[:mailer_settings] = options.delete(mailer_settings_key)
|
|
139
164
|
|
|
140
|
-
|
|
141
|
-
:sender_address, :exception_recipients,
|
|
142
|
-
:pre_callback, :post_callback,
|
|
143
|
-
:email_prefix, :email_format, :sections, :background_sections,
|
|
144
|
-
:verbose_subject, :normalize_subject, :delivery_method, :mailer_settings,
|
|
145
|
-
:email_headers, :mailer_parent, :template_path, :deliver_with].include?(k)}.each{|k,v| send("#{k}=", v)}
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def options
|
|
149
|
-
@options ||= {}.tap do |opts|
|
|
150
|
-
self.instance_variables.each { |var| opts[var[1..-1].to_sym] = self.instance_variable_get(var) }
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def mailer
|
|
155
|
-
@mailer ||= Class.new(mailer_parent.constantize).tap do |mailer|
|
|
156
|
-
mailer.extend(EmailNotifier::Mailer)
|
|
157
|
-
mailer.mailer_name = template_path
|
|
158
|
-
end
|
|
165
|
+
@base_options = DEFAULT_OPTIONS.merge(options)
|
|
159
166
|
end
|
|
160
167
|
|
|
161
|
-
def call(exception, options={})
|
|
168
|
+
def call(exception, options = {})
|
|
162
169
|
message = create_email(exception, options)
|
|
163
170
|
|
|
164
|
-
|
|
165
|
-
if deliver_with == :default
|
|
166
|
-
if message.respond_to?(:deliver_now)
|
|
167
|
-
message.deliver_now
|
|
168
|
-
else
|
|
169
|
-
message.deliver
|
|
170
|
-
end
|
|
171
|
-
else
|
|
172
|
-
message.send(deliver_with)
|
|
173
|
-
end
|
|
171
|
+
message.send(base_options[:deliver_with] || default_deliver_with(message))
|
|
174
172
|
end
|
|
175
173
|
|
|
176
|
-
def create_email(exception, options={})
|
|
174
|
+
def create_email(exception, options = {})
|
|
177
175
|
env = options[:env]
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
|
|
177
|
+
send_notice(exception, options, nil, base_options) do |_, default_opts|
|
|
178
|
+
if env.nil?
|
|
181
179
|
mailer.background_exception_notification(exception, options, default_opts)
|
|
182
|
-
|
|
183
|
-
else
|
|
184
|
-
send_notice(exception, options, nil, default_options) do |_, default_opts|
|
|
180
|
+
else
|
|
185
181
|
mailer.exception_notification(env, exception, options, default_opts)
|
|
186
182
|
end
|
|
187
183
|
end
|
|
188
184
|
end
|
|
189
185
|
|
|
190
|
-
def self.default_options
|
|
191
|
-
{
|
|
192
|
-
:sender_address => %("Exception Notifier" <exception.notifier@example.com>),
|
|
193
|
-
:exception_recipients => [],
|
|
194
|
-
:email_prefix => "[ERROR] ",
|
|
195
|
-
:email_format => :text,
|
|
196
|
-
:sections => %w(request session environment backtrace),
|
|
197
|
-
:background_sections => %w(backtrace data),
|
|
198
|
-
:verbose_subject => true,
|
|
199
|
-
:normalize_subject => false,
|
|
200
|
-
:delivery_method => nil,
|
|
201
|
-
:mailer_settings => nil,
|
|
202
|
-
:email_headers => {},
|
|
203
|
-
:mailer_parent => 'ActionMailer::Base',
|
|
204
|
-
:template_path => 'exception_notifier',
|
|
205
|
-
:deliver_with => :default
|
|
206
|
-
}
|
|
207
|
-
end
|
|
208
|
-
|
|
209
186
|
def self.normalize_digits(string)
|
|
210
187
|
string.gsub(/[0-9]+/, 'N')
|
|
211
188
|
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def mailer
|
|
193
|
+
@mailer ||= Class.new(base_options[:mailer_parent].constantize).tap do |mailer|
|
|
194
|
+
mailer.extend(EmailNotifier::Mailer)
|
|
195
|
+
mailer.mailer_name = base_options[:template_path]
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def default_deliver_with(message)
|
|
200
|
+
# FIXME: use `if Gem::Version.new(ActionMailer::VERSION::STRING) < Gem::Version.new('4.1')`
|
|
201
|
+
message.respond_to?(:deliver_now) ? :deliver_now : :deliver
|
|
202
|
+
end
|
|
212
203
|
end
|
|
213
204
|
end
|