exception_notification 4.2.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 +4 -3
- data/CHANGELOG.rdoc +57 -1
- data/CONTRIBUTING.md +21 -2
- data/Gemfile +3 -1
- data/README.md +106 -789
- 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 +34 -27
- 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 +75 -32
- 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 +19 -16
- data/lib/generators/exception_notification/install_generator.rb +11 -5
- data/lib/generators/exception_notification/templates/{exception_notification.rb → exception_notification.rb.erb} +14 -12
- 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 +66 -39
- data/test/exception_notifier/datadog_notifier_test.rb +153 -0
- data/test/exception_notifier/email_notifier_test.rb +301 -145
- data/test/exception_notifier/google_chat_notifier_test.rb +185 -0
- data/test/exception_notifier/hipchat_notifier_test.rb +112 -65
- 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 -6
- data/test/exception_notifier/slack_notifier_test.rb +109 -59
- 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 +68 -38
- 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 -38
- 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
|
-
:from
|
107
|
-
:
|
108
|
-
:
|
128
|
+
delivery_method: @options[:delivery_method],
|
129
|
+
to: exception_recipients,
|
130
|
+
from: self.class.default[: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
|