exception_notification 3.0.1 → 4.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Appraisals +7 -0
- data/CHANGELOG.rdoc +129 -1
- data/CODE_OF_CONDUCT.md +22 -0
- data/CONTRIBUTING.md +29 -1
- data/Gemfile +1 -1
- data/MIT-LICENSE +23 -0
- data/README.md +168 -222
- data/Rakefile +5 -11
- 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 +54 -0
- data/examples/sinatra/Gemfile +8 -0
- data/examples/sinatra/Gemfile.lock +95 -0
- data/examples/sinatra/Procfile +2 -0
- data/examples/sinatra/README.md +11 -0
- data/examples/sinatra/config.ru +3 -0
- data/examples/sinatra/sinatra_app.rb +36 -0
- data/exception_notification.gemspec +32 -11
- data/gemfiles/rails4_0.gemfile +7 -0
- data/gemfiles/rails4_1.gemfile +7 -0
- data/gemfiles/rails4_2.gemfile +7 -0
- data/gemfiles/rails5_0.gemfile +7 -0
- 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 +11 -0
- data/lib/exception_notification/rack.rb +55 -0
- data/lib/exception_notification/rails.rb +9 -0
- data/lib/exception_notification/resque.rb +22 -0
- data/lib/exception_notification/sidekiq.rb +27 -0
- data/lib/exception_notification/version.rb +3 -0
- data/lib/exception_notifier.rb +137 -61
- data/lib/exception_notifier/base_notifier.rb +24 -0
- data/lib/exception_notifier/campfire_notifier.rb +16 -11
- data/lib/exception_notifier/datadog_notifier.rb +153 -0
- data/lib/exception_notifier/email_notifier.rb +196 -0
- data/lib/exception_notifier/google_chat_notifier.rb +42 -0
- data/lib/exception_notifier/hipchat_notifier.rb +49 -0
- data/lib/exception_notifier/irc_notifier.rb +57 -0
- data/lib/exception_notifier/mattermost_notifier.rb +72 -0
- data/lib/exception_notifier/modules/backtrace_cleaner.rb +11 -0
- data/lib/exception_notifier/modules/error_grouping.rb +77 -0
- data/lib/exception_notifier/modules/formatter.rb +118 -0
- data/lib/exception_notifier/notifier.rb +9 -179
- data/lib/exception_notifier/slack_notifier.rb +111 -0
- data/lib/exception_notifier/sns_notifier.rb +85 -0
- data/lib/exception_notifier/teams_notifier.rb +193 -0
- data/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +3 -1
- data/lib/exception_notifier/views/exception_notifier/_data.html.erb +6 -1
- data/lib/exception_notifier/views/exception_notifier/_environment.html.erb +8 -6
- data/lib/exception_notifier/views/exception_notifier/_environment.text.erb +1 -4
- data/lib/exception_notifier/views/exception_notifier/_request.html.erb +36 -5
- data/lib/exception_notifier/views/exception_notifier/_request.text.erb +10 -5
- data/lib/exception_notifier/views/exception_notifier/_session.html.erb +10 -2
- data/lib/exception_notifier/views/exception_notifier/_session.text.erb +2 -2
- data/lib/exception_notifier/views/exception_notifier/_title.html.erb +3 -3
- data/lib/exception_notifier/views/exception_notifier/background_exception_notification.html.erb +38 -11
- data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +10 -11
- data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +38 -22
- data/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +2 -3
- data/lib/exception_notifier/webhook_notifier.rb +51 -0
- data/lib/generators/exception_notification/install_generator.rb +15 -0
- data/lib/generators/exception_notification/templates/exception_notification.rb.erb +55 -0
- data/test/exception_notification/rack_test.rb +60 -0
- data/test/exception_notification/resque_test.rb +52 -0
- data/test/exception_notifier/campfire_notifier_test.rb +120 -0
- data/test/exception_notifier/datadog_notifier_test.rb +151 -0
- data/test/exception_notifier/email_notifier_test.rb +351 -0
- data/test/exception_notifier/google_chat_notifier_test.rb +181 -0
- data/test/exception_notifier/hipchat_notifier_test.rb +218 -0
- data/test/exception_notifier/irc_notifier_test.rb +137 -0
- data/test/exception_notifier/mattermost_notifier_test.rb +202 -0
- data/test/exception_notifier/modules/error_grouping_test.rb +165 -0
- data/test/exception_notifier/modules/formatter_test.rb +150 -0
- data/test/exception_notifier/sidekiq_test.rb +38 -0
- data/test/exception_notifier/slack_notifier_test.rb +227 -0
- data/test/exception_notifier/sns_notifier_test.rb +121 -0
- data/test/exception_notifier/teams_notifier_test.rb +90 -0
- data/test/exception_notifier/webhook_notifier_test.rb +96 -0
- data/test/exception_notifier_test.rb +182 -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 +12 -8
- metadata +333 -164
- data/.gemtest +0 -0
- data/Gemfile.lock +0 -122
- data/test/background_exception_notification_test.rb +0 -82
- data/test/campfire_test.rb +0 -53
- data/test/dummy/.gitignore +0 -4
- data/test/dummy/Gemfile +0 -33
- data/test/dummy/Gemfile.lock +0 -118
- 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 -13
- data/test/dummy/config/environments/development.rb +0 -24
- data/test/dummy/config/environments/production.rb +0 -49
- 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 -7
- 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/fixtures/posts.yml +0 -11
- data/test/dummy/test/functional/posts_controller_test.rb +0 -239
- data/test/dummy/test/test_helper.rb +0 -13
- data/test/exception_notification_test.rb +0 -73
@@ -0,0 +1,9 @@
|
|
1
|
+
module ExceptionNotification
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
config.exception_notification = ExceptionNotifier
|
4
|
+
config.exception_notification.logger = Rails.logger
|
5
|
+
config.exception_notification.error_grouping_cache = Rails.cache
|
6
|
+
|
7
|
+
config.app_middleware.use ExceptionNotification::Rack
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'resque/failure/base'
|
2
|
+
|
3
|
+
module ExceptionNotification
|
4
|
+
class Resque < Resque::Failure::Base
|
5
|
+
def self.count
|
6
|
+
::Resque::Stat[:failed]
|
7
|
+
end
|
8
|
+
|
9
|
+
def save
|
10
|
+
data = {
|
11
|
+
error_class: exception.class.name,
|
12
|
+
error_message: exception.message,
|
13
|
+
failed_at: Time.now.to_s,
|
14
|
+
payload: payload,
|
15
|
+
queue: queue,
|
16
|
+
worker: worker.to_s
|
17
|
+
}
|
18
|
+
|
19
|
+
ExceptionNotifier.notify_exception(exception, data: { resque: data })
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
|
3
|
+
# Note: this class is only needed for Sidekiq version < 3.
|
4
|
+
module ExceptionNotification
|
5
|
+
class Sidekiq
|
6
|
+
def call(_worker, msg, _queue)
|
7
|
+
yield
|
8
|
+
rescue Exception => exception
|
9
|
+
ExceptionNotifier.notify_exception(exception, data: { sidekiq: msg })
|
10
|
+
raise exception
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
if ::Sidekiq::VERSION < '3'
|
16
|
+
::Sidekiq.configure_server do |config|
|
17
|
+
config.server_middleware do |chain|
|
18
|
+
chain.add ::ExceptionNotification::Sidekiq
|
19
|
+
end
|
20
|
+
end
|
21
|
+
else
|
22
|
+
::Sidekiq.configure_server do |config|
|
23
|
+
config.error_handlers << proc do |ex, context|
|
24
|
+
ExceptionNotifier.notify_exception(ex, data: { sidekiq: context })
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/exception_notifier.rb
CHANGED
@@ -1,75 +1,151 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require '
|
1
|
+
require 'logger'
|
2
|
+
require 'active_support/core_ext/string/inflections'
|
3
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
4
|
+
require 'exception_notifier/base_notifier'
|
5
|
+
require 'exception_notifier/modules/error_grouping'
|
4
6
|
|
5
|
-
|
7
|
+
module ExceptionNotifier
|
8
|
+
include ErrorGrouping
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
autoload :BacktraceCleaner, 'exception_notifier/modules/backtrace_cleaner'
|
11
|
+
autoload :Formatter, 'exception_notifier/modules/formatter'
|
12
|
+
|
13
|
+
autoload :Notifier, 'exception_notifier/notifier'
|
14
|
+
autoload :EmailNotifier, 'exception_notifier/email_notifier'
|
15
|
+
autoload :CampfireNotifier, 'exception_notifier/campfire_notifier'
|
16
|
+
autoload :HipchatNotifier, 'exception_notifier/hipchat_notifier'
|
17
|
+
autoload :WebhookNotifier, 'exception_notifier/webhook_notifier'
|
18
|
+
autoload :IrcNotifier, 'exception_notifier/irc_notifier'
|
19
|
+
autoload :SlackNotifier, 'exception_notifier/slack_notifier'
|
20
|
+
autoload :MattermostNotifier, 'exception_notifier/mattermost_notifier'
|
21
|
+
autoload :TeamsNotifier, 'exception_notifier/teams_notifier'
|
22
|
+
autoload :SnsNotifier, 'exception_notifier/sns_notifier'
|
23
|
+
autoload :GoogleChatNotifier, 'exception_notifier/google_chat_notifier'
|
24
|
+
autoload :DatadogNotifier, 'exception_notifier/datadog_notifier'
|
25
|
+
|
26
|
+
class UndefinedNotifierError < StandardError; end
|
27
|
+
|
28
|
+
# Define logger
|
29
|
+
mattr_accessor :logger
|
30
|
+
@@logger = Logger.new(STDOUT)
|
31
|
+
|
32
|
+
# Define a set of exceptions to be ignored, ie, dont send notifications when any of them are raised.
|
33
|
+
mattr_accessor :ignored_exceptions
|
34
|
+
@@ignored_exceptions = %w[ActiveRecord::RecordNotFound Mongoid::Errors::DocumentNotFound AbstractController::ActionNotFound ActionController::RoutingError ActionController::UnknownFormat ActionController::UrlGenerationError]
|
35
|
+
|
36
|
+
mattr_accessor :testing_mode
|
37
|
+
@@testing_mode = false
|
38
|
+
|
39
|
+
class << self
|
40
|
+
# Store conditions that decide when exceptions must be ignored or not.
|
41
|
+
@@ignores = []
|
42
|
+
|
43
|
+
# Store notifiers that send notifications when exceptions are raised.
|
44
|
+
@@notifiers = {}
|
45
|
+
|
46
|
+
def testing_mode!
|
47
|
+
self.testing_mode = true
|
12
48
|
end
|
13
|
-
end
|
14
49
|
|
15
|
-
|
16
|
-
|
17
|
-
|
50
|
+
def notify_exception(exception, options = {}, &block)
|
51
|
+
return false if ignored_exception?(options[:ignore_exceptions], exception)
|
52
|
+
return false if ignored?(exception, options)
|
18
53
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
Notifier.default_exception_recipients = @options[:exception_recipients]
|
24
|
-
Notifier.default_email_prefix = @options[:email_prefix]
|
25
|
-
Notifier.default_email_format = @options[:email_format]
|
26
|
-
Notifier.default_sections = @options[:sections]
|
27
|
-
Notifier.default_background_sections = @options[:background_sections]
|
28
|
-
Notifier.default_verbose_subject = @options[:verbose_subject]
|
29
|
-
Notifier.default_normalize_subject = @options[:normalize_subject]
|
30
|
-
Notifier.default_smtp_settings = @options[:smtp_settings]
|
31
|
-
Notifier.default_email_headers = @options[:email_headers]
|
32
|
-
|
33
|
-
@campfire = CampfireNotifier.new @options[:campfire]
|
34
|
-
|
35
|
-
@options[:ignore_exceptions] ||= self.class.default_ignore_exceptions
|
36
|
-
@options[:ignore_crawlers] ||= self.class.default_ignore_crawlers
|
37
|
-
@options[:ignore_if] ||= lambda { |env, e| false }
|
38
|
-
end
|
54
|
+
if error_grouping
|
55
|
+
errors_count = group_error!(exception, options)
|
56
|
+
return false unless send_notification?(exception, errors_count)
|
57
|
+
end
|
39
58
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
unless ignored_exception(options[:ignore_exceptions], exception) ||
|
47
|
-
from_crawler(options[:ignore_crawlers], env['HTTP_USER_AGENT']) ||
|
48
|
-
conditionally_ignored(options[:ignore_if], env, exception)
|
49
|
-
Notifier.exception_notification(env, exception).deliver
|
50
|
-
@campfire.exception_notification(exception)
|
51
|
-
env['exception_notifier.delivered'] = true
|
59
|
+
selected_notifiers = options.delete(:notifiers) || notifiers
|
60
|
+
[*selected_notifiers].each do |notifier|
|
61
|
+
fire_notification(notifier, exception, options.dup, &block)
|
62
|
+
end
|
63
|
+
true
|
52
64
|
end
|
53
65
|
|
54
|
-
|
55
|
-
|
66
|
+
def register_exception_notifier(name, notifier_or_options)
|
67
|
+
if notifier_or_options.respond_to?(:call)
|
68
|
+
@@notifiers[name] = notifier_or_options
|
69
|
+
elsif notifier_or_options.is_a?(Hash)
|
70
|
+
create_and_register_notifier(name, notifier_or_options)
|
71
|
+
else
|
72
|
+
raise ArgumentError, "Invalid notifier '#{name}' defined as #{notifier_or_options.inspect}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
alias add_notifier register_exception_notifier
|
56
76
|
|
57
|
-
|
77
|
+
def unregister_exception_notifier(name)
|
78
|
+
@@notifiers.delete(name)
|
79
|
+
end
|
58
80
|
|
59
|
-
|
60
|
-
|
61
|
-
|
81
|
+
def registered_exception_notifier(name)
|
82
|
+
@@notifiers[name]
|
83
|
+
end
|
62
84
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
end unless ignore_array.blank?
|
67
|
-
false
|
68
|
-
end
|
85
|
+
def notifiers
|
86
|
+
@@notifiers.keys
|
87
|
+
end
|
69
88
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
89
|
+
# Adds a condition to decide when an exception must be ignored or not.
|
90
|
+
#
|
91
|
+
# ExceptionNotifier.ignore_if do |exception, options|
|
92
|
+
# not Rails.env.production?
|
93
|
+
# end
|
94
|
+
def ignore_if(&block)
|
95
|
+
@@ignores << block
|
96
|
+
end
|
97
|
+
|
98
|
+
def ignore_crawlers(crawlers)
|
99
|
+
ignore_if do |_exception, opts|
|
100
|
+
opts.key?(:env) && from_crawler(opts[:env], crawlers)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def clear_ignore_conditions!
|
105
|
+
@@ignores.clear
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def ignored?(exception, options)
|
111
|
+
@@ignores.any? { |condition| condition.call(exception, options) }
|
112
|
+
rescue Exception => e
|
113
|
+
raise e if @@testing_mode
|
114
|
+
|
115
|
+
logger.warn "An error occurred when evaluating an ignore condition. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
116
|
+
false
|
117
|
+
end
|
118
|
+
|
119
|
+
def ignored_exception?(ignore_array, exception)
|
120
|
+
all_ignored_exceptions = (Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s)
|
121
|
+
exception_ancestors = exception.class.ancestors.map(&:to_s)
|
122
|
+
!(all_ignored_exceptions & exception_ancestors).empty?
|
123
|
+
end
|
124
|
+
|
125
|
+
def fire_notification(notifier_name, exception, options, &block)
|
126
|
+
notifier = registered_exception_notifier(notifier_name)
|
127
|
+
notifier.call(exception, options, &block)
|
128
|
+
rescue Exception => e
|
129
|
+
raise e if @@testing_mode
|
130
|
+
|
131
|
+
logger.warn "An error occurred when sending a notification using '#{notifier_name}' notifier. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
132
|
+
false
|
133
|
+
end
|
134
|
+
|
135
|
+
def create_and_register_notifier(name, options)
|
136
|
+
notifier_classname = "#{name}_notifier".camelize
|
137
|
+
notifier_class = ExceptionNotifier.const_get(notifier_classname)
|
138
|
+
notifier = notifier_class.new(options)
|
139
|
+
register_exception_notifier(name, notifier)
|
140
|
+
rescue NameError => e
|
141
|
+
raise UndefinedNotifierError, "No notifier named '#{name}' was found. Please, revise your configuration options. Cause: #{e.message}"
|
142
|
+
end
|
143
|
+
|
144
|
+
def from_crawler(env, ignored_crawlers)
|
145
|
+
agent = env['HTTP_USER_AGENT']
|
146
|
+
Array(ignored_crawlers).any? do |crawler|
|
147
|
+
agent =~ Regexp.new(crawler)
|
148
|
+
end
|
149
|
+
end
|
74
150
|
end
|
75
151
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ExceptionNotifier
|
2
|
+
class BaseNotifier
|
3
|
+
attr_accessor :base_options
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@base_options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def send_notice(exception, options, message, message_opts = nil)
|
10
|
+
_pre_callback(exception, options, message, message_opts)
|
11
|
+
result = yield(message, message_opts)
|
12
|
+
_post_callback(exception, options, message, message_opts)
|
13
|
+
result
|
14
|
+
end
|
15
|
+
|
16
|
+
def _pre_callback(exception, options, message, message_opts)
|
17
|
+
@base_options[:pre_callback].call(options, self, exception.backtrace, message, message_opts) if @base_options[:pre_callback].respond_to?(:call)
|
18
|
+
end
|
19
|
+
|
20
|
+
def _post_callback(exception, options, message, message_opts)
|
21
|
+
@base_options[:post_callback].call(options, self, exception.backtrace, message, message_opts) if @base_options[:post_callback].respond_to?(:call)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,26 +1,33 @@
|
|
1
|
-
|
2
|
-
class CampfireNotifier
|
3
|
-
cattr_accessor :tinder_available, true
|
4
|
-
|
1
|
+
module ExceptionNotifier
|
2
|
+
class CampfireNotifier < BaseNotifier
|
5
3
|
attr_accessor :subdomain
|
6
4
|
attr_accessor :token
|
7
5
|
attr_accessor :room
|
8
6
|
|
9
7
|
def initialize(options)
|
8
|
+
super
|
10
9
|
begin
|
11
|
-
return unless tinder_available
|
12
|
-
|
13
10
|
subdomain = options.delete(:subdomain)
|
14
11
|
room_name = options.delete(:room_name)
|
15
12
|
@campfire = Tinder::Campfire.new subdomain, options
|
16
13
|
@room = @campfire.find_room_by_name room_name
|
17
|
-
rescue
|
14
|
+
rescue StandardError
|
18
15
|
@campfire = @room = nil
|
19
16
|
end
|
20
17
|
end
|
21
18
|
|
22
|
-
def
|
23
|
-
|
19
|
+
def call(exception, options = {})
|
20
|
+
return unless active?
|
21
|
+
|
22
|
+
message = if options[:accumulated_errors_count].to_i > 1
|
23
|
+
"The exception occurred #{options[:accumulated_errors_count]} times: '#{exception.message}'"
|
24
|
+
else
|
25
|
+
"A new exception occurred: '#{exception.message}'"
|
26
|
+
end
|
27
|
+
message += " on '#{exception.backtrace.first}'" if exception.backtrace
|
28
|
+
send_notice(exception, options, message) do |msg, _|
|
29
|
+
@room.paste msg
|
30
|
+
end
|
24
31
|
end
|
25
32
|
|
26
33
|
private
|
@@ -30,5 +37,3 @@ class ExceptionNotifier
|
|
30
37
|
end
|
31
38
|
end
|
32
39
|
end
|
33
|
-
|
34
|
-
ExceptionNotifier::CampfireNotifier.tinder_available = Gem.loaded_specs.keys.include? 'tinder'
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'action_dispatch'
|
2
|
+
|
3
|
+
module ExceptionNotifier
|
4
|
+
class DatadogNotifier < BaseNotifier
|
5
|
+
attr_reader :client,
|
6
|
+
:default_options
|
7
|
+
|
8
|
+
def initialize(options)
|
9
|
+
super
|
10
|
+
@client = options.fetch(:client)
|
11
|
+
@default_options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(exception, options = {})
|
15
|
+
client.emit_event(
|
16
|
+
datadog_event(exception, options)
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def datadog_event(exception, options = {})
|
21
|
+
DatadogExceptionEvent.new(
|
22
|
+
exception,
|
23
|
+
options.reverse_merge(default_options)
|
24
|
+
).event
|
25
|
+
end
|
26
|
+
|
27
|
+
class DatadogExceptionEvent
|
28
|
+
include ExceptionNotifier::BacktraceCleaner
|
29
|
+
|
30
|
+
MAX_TITLE_LENGTH = 120
|
31
|
+
MAX_VALUE_LENGTH = 300
|
32
|
+
MAX_BACKTRACE_SIZE = 3
|
33
|
+
ALERT_TYPE = 'error'.freeze
|
34
|
+
|
35
|
+
attr_reader :exception,
|
36
|
+
:options
|
37
|
+
|
38
|
+
def initialize(exception, options)
|
39
|
+
@exception = exception
|
40
|
+
@options = options
|
41
|
+
end
|
42
|
+
|
43
|
+
def request
|
44
|
+
@request ||= ActionDispatch::Request.new(options[:env]) if options[:env]
|
45
|
+
end
|
46
|
+
|
47
|
+
def controller
|
48
|
+
@controller ||= options[:env] && options[:env]['action_controller.instance']
|
49
|
+
end
|
50
|
+
|
51
|
+
def backtrace
|
52
|
+
@backtrace ||= exception.backtrace ? clean_backtrace(exception) : []
|
53
|
+
end
|
54
|
+
|
55
|
+
def tags
|
56
|
+
options[:tags] || []
|
57
|
+
end
|
58
|
+
|
59
|
+
def title_prefix
|
60
|
+
options[:title_prefix] || ''
|
61
|
+
end
|
62
|
+
|
63
|
+
def event
|
64
|
+
title = formatted_title
|
65
|
+
body = formatted_body
|
66
|
+
|
67
|
+
Dogapi::Event.new(
|
68
|
+
body,
|
69
|
+
msg_title: title,
|
70
|
+
alert_type: ALERT_TYPE,
|
71
|
+
tags: tags,
|
72
|
+
aggregation_key: [title]
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
def formatted_title
|
77
|
+
title = ''
|
78
|
+
title << title_prefix
|
79
|
+
title << "#{controller.controller_name} #{controller.action_name}" if controller
|
80
|
+
title << " (#{exception.class})"
|
81
|
+
title << " #{exception.message.inspect}"
|
82
|
+
|
83
|
+
truncate(title, MAX_TITLE_LENGTH)
|
84
|
+
end
|
85
|
+
|
86
|
+
def formatted_body
|
87
|
+
text = []
|
88
|
+
|
89
|
+
text << '%%%'
|
90
|
+
text << formatted_request if request
|
91
|
+
text << formatted_session if request
|
92
|
+
text << formatted_backtrace
|
93
|
+
text << '%%%'
|
94
|
+
|
95
|
+
text.join("\n")
|
96
|
+
end
|
97
|
+
|
98
|
+
def formatted_key_value(key, value)
|
99
|
+
"**#{key}:** #{value}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def formatted_request
|
103
|
+
text = []
|
104
|
+
text << '### **Request**'
|
105
|
+
text << formatted_key_value('URL', request.url)
|
106
|
+
text << formatted_key_value('HTTP Method', request.request_method)
|
107
|
+
text << formatted_key_value('IP Address', request.remote_ip)
|
108
|
+
text << formatted_key_value('Parameters', request.filtered_parameters.inspect)
|
109
|
+
text << formatted_key_value('Timestamp', Time.current)
|
110
|
+
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
|
114
|
+
text << formatted_key_value('Process', $PROCESS_ID)
|
115
|
+
text << '___'
|
116
|
+
text.join("\n")
|
117
|
+
end
|
118
|
+
|
119
|
+
def formatted_session
|
120
|
+
text = []
|
121
|
+
text << '### **Session**'
|
122
|
+
text << formatted_key_value('Data', request.session.to_hash)
|
123
|
+
text << '___'
|
124
|
+
text.join("\n")
|
125
|
+
end
|
126
|
+
|
127
|
+
def formatted_backtrace
|
128
|
+
size = [backtrace.size, MAX_BACKTRACE_SIZE].min
|
129
|
+
|
130
|
+
text = []
|
131
|
+
text << '### **Backtrace**'
|
132
|
+
text << '````'
|
133
|
+
size.times { |i| text << backtrace[i] }
|
134
|
+
text << '````'
|
135
|
+
text << '___'
|
136
|
+
text.join("\n")
|
137
|
+
end
|
138
|
+
|
139
|
+
def truncate(string, max)
|
140
|
+
string.length > max ? "#{string[0...max]}..." : string
|
141
|
+
end
|
142
|
+
|
143
|
+
def inspect_object(object)
|
144
|
+
case object
|
145
|
+
when Hash, Array
|
146
|
+
truncate(object.inspect, MAX_VALUE_LENGTH)
|
147
|
+
else
|
148
|
+
object.to_s
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|