exception-track 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/exception-track/version.rb +1 -1
- data/lib/exception-track.rb +1 -2
- data/lib/exception_notification/rack.rb +66 -0
- data/lib/exception_notification/rails.rb +11 -0
- data/lib/exception_notification/resque.rb +24 -0
- data/lib/exception_notification/sidekiq.rb +29 -0
- data/lib/exception_notification/version.rb +5 -0
- data/lib/exception_notification.rb +14 -0
- data/lib/exception_notifier/base_notifier.rb +30 -0
- data/lib/exception_notifier/{exception_track_notifier.rb → db_notifier.rb} +13 -11
- data/lib/exception_notifier/modules/backtrace_cleaner.rb +13 -0
- data/lib/exception_notifier/modules/error_grouping.rb +90 -0
- data/lib/exception_notifier/modules/formatter.rb +125 -0
- data/lib/exception_notifier/notifier.rb +191 -0
- metadata +97 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 71a2bc45d225661a466bdd4fe3ae8227253cf0e4e48430a3f3db33605c04aed7
|
4
|
+
data.tar.gz: fbf989949bd59b5ac8ea1b44c159cae09ded01832c21c42193729a95b20f3015
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a7b776ccbda802c32ca77f60ec4257e198d2e9e96c611ccc03c14fb00efc1fb5f127a60e9df627b985ddef347d1e1ceb2f04c8919894ab73da3ac796fe8cc0c5
|
7
|
+
data.tar.gz: 14a4f70638b02742170ff3138d553624a8c379b2c0480624f178354fd0324dbcb58d5d75627e78f24026338fbeb750478431034ac76eb3b7cb3f8cd42df4dc9e
|
data/lib/exception-track.rb
CHANGED
@@ -7,7 +7,6 @@ require "exception-track/engine"
|
|
7
7
|
|
8
8
|
require "exception_notification"
|
9
9
|
require "exception_notification/rails"
|
10
|
-
require "exception_notifier/exception_track_notifier"
|
11
10
|
|
12
11
|
require "kaminari"
|
13
12
|
|
@@ -28,5 +27,5 @@ module ExceptionTrack
|
|
28
27
|
end
|
29
28
|
|
30
29
|
ExceptionNotification.configure do |config|
|
31
|
-
config.add_notifier :
|
30
|
+
config.add_notifier :db, {}
|
32
31
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExceptionNotification
|
4
|
+
class Rack
|
5
|
+
class CascadePassException < RuntimeError; end
|
6
|
+
|
7
|
+
def initialize(app, options = {})
|
8
|
+
@app = app
|
9
|
+
|
10
|
+
ExceptionNotifier.tap do |en|
|
11
|
+
en.ignored_exceptions = options.delete(:ignore_exceptions) if options.key?(:ignore_exceptions)
|
12
|
+
en.error_grouping = options.delete(:error_grouping) if options.key?(:error_grouping)
|
13
|
+
en.error_grouping_period = options.delete(:error_grouping_period) if options.key?(:error_grouping_period)
|
14
|
+
en.notification_trigger = options.delete(:notification_trigger) if options.key?(:notification_trigger)
|
15
|
+
|
16
|
+
if options.key?(:error_grouping_cache)
|
17
|
+
en.error_grouping_cache = options.delete(:error_grouping_cache)
|
18
|
+
elsif defined?(Rails) && Rails.respond_to?(:cache)
|
19
|
+
en.error_grouping_cache = Rails.cache
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
if options.key?(:ignore_if)
|
24
|
+
rack_ignore = options.delete(:ignore_if)
|
25
|
+
ExceptionNotifier.ignore_if do |exception, opts|
|
26
|
+
opts.key?(:env) && rack_ignore.call(opts[:env], exception)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if options.key?(:ignore_notifier_if)
|
31
|
+
rack_ignore_by_notifier = options.delete(:ignore_notifier_if)
|
32
|
+
rack_ignore_by_notifier.each do |notifier, proc|
|
33
|
+
ExceptionNotifier.ignore_notifier_if(notifier) do |exception, opts|
|
34
|
+
opts.key?(:env) && proc.call(opts[:env], exception)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
ExceptionNotifier.ignore_crawlers(options.delete(:ignore_crawlers)) if options.key?(:ignore_crawlers)
|
40
|
+
|
41
|
+
@ignore_cascade_pass = options.delete(:ignore_cascade_pass) { true }
|
42
|
+
|
43
|
+
options.each do |notifier_name, opts|
|
44
|
+
ExceptionNotifier.register_exception_notifier(notifier_name, opts)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def call(env)
|
49
|
+
_, headers, = response = @app.call(env)
|
50
|
+
|
51
|
+
if !@ignore_cascade_pass && headers["X-Cascade"] == "pass"
|
52
|
+
msg = "This exception means that the preceding Rack middleware set the 'X-Cascade' header to 'pass' -- in " \
|
53
|
+
"Rails, this often means that the route was not found (404 error)."
|
54
|
+
raise CascadePassException, msg
|
55
|
+
end
|
56
|
+
|
57
|
+
response
|
58
|
+
rescue Exception => e
|
59
|
+
env["exception_notifier.delivered"] = true if ExceptionNotifier.notify_exception(e, env: env)
|
60
|
+
|
61
|
+
raise e unless e.is_a?(CascadePassException)
|
62
|
+
|
63
|
+
response
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExceptionNotification
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
config.exception_notification = ExceptionNotifier
|
6
|
+
config.exception_notification.logger = Rails.logger
|
7
|
+
config.exception_notification.error_grouping_cache = Rails.cache
|
8
|
+
|
9
|
+
config.app_middleware.use ExceptionNotification::Rack
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "resque/failure/base"
|
4
|
+
|
5
|
+
module ExceptionNotification
|
6
|
+
class Resque < Resque::Failure::Base
|
7
|
+
def self.count
|
8
|
+
::Resque::Stat[:failed]
|
9
|
+
end
|
10
|
+
|
11
|
+
def save
|
12
|
+
data = {
|
13
|
+
error_class: exception.class.name,
|
14
|
+
error_message: exception.message,
|
15
|
+
failed_at: Time.now.to_s,
|
16
|
+
payload: payload,
|
17
|
+
queue: queue,
|
18
|
+
worker: worker.to_s
|
19
|
+
}
|
20
|
+
|
21
|
+
ExceptionNotifier.notify_exception(exception, data: {resque: data})
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sidekiq"
|
4
|
+
|
5
|
+
# Note: this class is only needed for Sidekiq version < 3.
|
6
|
+
module ExceptionNotification
|
7
|
+
class Sidekiq
|
8
|
+
def call(_worker, msg, _queue)
|
9
|
+
yield
|
10
|
+
rescue Exception => e
|
11
|
+
ExceptionNotifier.notify_exception(e, data: {sidekiq: msg})
|
12
|
+
raise e
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
if ::Sidekiq::VERSION < "3"
|
18
|
+
::Sidekiq.configure_server do |config|
|
19
|
+
config.server_middleware do |chain|
|
20
|
+
chain.add ::ExceptionNotification::Sidekiq
|
21
|
+
end
|
22
|
+
end
|
23
|
+
else
|
24
|
+
::Sidekiq.configure_server do |config|
|
25
|
+
config.error_handlers << proc do |ex, context|
|
26
|
+
ExceptionNotifier.notify_exception(ex, data: {sidekiq: context})
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "exception_notifier/notifier"
|
4
|
+
require "exception_notification/rack"
|
5
|
+
require "exception_notification/version"
|
6
|
+
|
7
|
+
module ExceptionNotification
|
8
|
+
# Alternative way to setup ExceptionNotification.
|
9
|
+
# Run 'rails generate exception_notification:install' to create
|
10
|
+
# a fresh initializer with all configuration values.
|
11
|
+
def self.configure
|
12
|
+
yield ExceptionNotifier
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExceptionNotifier
|
4
|
+
class BaseNotifier
|
5
|
+
attr_accessor :base_options
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@base_options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def send_notice(exception, options, message, message_opts = nil)
|
12
|
+
_pre_callback(exception, options, message, message_opts)
|
13
|
+
result = yield(message, message_opts)
|
14
|
+
_post_callback(exception, options, message, message_opts)
|
15
|
+
result
|
16
|
+
end
|
17
|
+
|
18
|
+
def _pre_callback(exception, options, message, message_opts)
|
19
|
+
return unless @base_options[:pre_callback].respond_to?(:call)
|
20
|
+
|
21
|
+
@base_options[:pre_callback].call(options, self, exception.backtrace, message, message_opts)
|
22
|
+
end
|
23
|
+
|
24
|
+
def _post_callback(exception, options, message, message_opts)
|
25
|
+
return unless @base_options[:post_callback].respond_to?(:call)
|
26
|
+
|
27
|
+
@base_options[:post_callback].call(options, self, exception.backtrace, message, message_opts)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ExceptionNotifier
|
4
|
-
class
|
5
|
-
def initialize(
|
4
|
+
class DbNotifier < ExceptionNotifier::BaseNotifier
|
5
|
+
def initialize(opts = {})
|
6
|
+
super(opts)
|
7
|
+
end
|
6
8
|
|
7
9
|
def call(exception, opts = {})
|
8
10
|
return unless ExceptionTrack.config.enabled_env?(Rails.env)
|
@@ -26,7 +28,7 @@ module ExceptionNotifier
|
|
26
28
|
ExceptionTrack::Log.create(title: title[0, 200], body: messages.join("\n"))
|
27
29
|
end
|
28
30
|
end
|
29
|
-
rescue
|
31
|
+
rescue => e
|
30
32
|
errs = []
|
31
33
|
errs << "-- [ExceptionTrack] create error ---------------------------"
|
32
34
|
errs << e.message.indent(2)
|
@@ -45,14 +47,14 @@ module ExceptionNotifier
|
|
45
47
|
parameters = filter_parameters(env)
|
46
48
|
|
47
49
|
headers = []
|
48
|
-
headers << "Method: #{env[
|
49
|
-
headers << "URL: #{env[
|
50
|
+
headers << "Method: #{env["REQUEST_METHOD"]}"
|
51
|
+
headers << "URL: #{env["REQUEST_URI"]}"
|
50
52
|
headers << "Parameters:\n#{pretty_hash(parameters.except(:controller, :action), 13)}" if env["REQUEST_METHOD"].downcase != "get"
|
51
|
-
headers << "Controller: #{parameters[
|
52
|
-
headers << "RequestId: #{env[
|
53
|
-
headers << "User-Agent: #{env[
|
54
|
-
headers << "Remote IP: #{env[
|
55
|
-
headers << "Language: #{env[
|
53
|
+
headers << "Controller: #{parameters["controller"]}##{parameters["action"]}"
|
54
|
+
headers << "RequestId: #{env["action_dispatch.request_id"]}"
|
55
|
+
headers << "User-Agent: #{env["HTTP_USER_AGENT"]}"
|
56
|
+
headers << "Remote IP: #{env["REMOTE_ADDR"]}"
|
57
|
+
headers << "Language: #{env["HTTP_ACCEPT_LANGUAGE"]}"
|
56
58
|
headers << "Server: #{Socket.gethostname}"
|
57
59
|
headers << "Process: #{$PROCESS_ID}"
|
58
60
|
|
@@ -63,7 +65,7 @@ module ExceptionNotifier
|
|
63
65
|
parameters = env["action_dispatch.request.parameters"] || {}
|
64
66
|
parameter_filter = ActiveSupport::ParameterFilter.new(env["action_dispatch.parameter_filter"] || [])
|
65
67
|
parameter_filter.filter(parameters)
|
66
|
-
rescue
|
68
|
+
rescue => e
|
67
69
|
Rails.logger.error "filter_parameters error: #{e.inspect}"
|
68
70
|
parameters
|
69
71
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExceptionNotifier
|
4
|
+
module BacktraceCleaner
|
5
|
+
def clean_backtrace(exception)
|
6
|
+
if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner)
|
7
|
+
Rails.backtrace_cleaner.send(:filter, exception.backtrace)
|
8
|
+
else
|
9
|
+
exception.backtrace
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
if Rails.version > "7.0"
|
4
|
+
require "active_support/isolated_execution_state"
|
5
|
+
end
|
6
|
+
require "active_support/core_ext/numeric/time"
|
7
|
+
require "active_support/concern"
|
8
|
+
|
9
|
+
module ExceptionNotifier
|
10
|
+
module ErrorGrouping
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do
|
14
|
+
mattr_accessor :error_grouping
|
15
|
+
self.error_grouping = false
|
16
|
+
|
17
|
+
mattr_accessor :error_grouping_period
|
18
|
+
self.error_grouping_period = 5.minutes
|
19
|
+
|
20
|
+
mattr_accessor :notification_trigger
|
21
|
+
|
22
|
+
mattr_accessor :error_grouping_cache
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
# Fallback to the memory store while the specified cache store doesn't work
|
27
|
+
#
|
28
|
+
def fallback_cache_store
|
29
|
+
@fallback_cache_store ||= ActiveSupport::Cache::MemoryStore.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def error_count(error_key)
|
33
|
+
count =
|
34
|
+
begin
|
35
|
+
error_grouping_cache.read(error_key)
|
36
|
+
rescue => e
|
37
|
+
log_cache_error(error_grouping_cache, e, :read)
|
38
|
+
fallback_cache_store.read(error_key)
|
39
|
+
end
|
40
|
+
|
41
|
+
count&.to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
def save_error_count(error_key, count)
|
45
|
+
error_grouping_cache.write(error_key, count, expires_in: error_grouping_period)
|
46
|
+
rescue => e
|
47
|
+
log_cache_error(error_grouping_cache, e, :write)
|
48
|
+
fallback_cache_store.write(error_key, count, expires_in: error_grouping_period)
|
49
|
+
end
|
50
|
+
|
51
|
+
def group_error!(exception, options)
|
52
|
+
message_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\nmessage:#{exception.message}")}"
|
53
|
+
accumulated_errors_count = 1
|
54
|
+
|
55
|
+
if (count = error_count(message_based_key))
|
56
|
+
accumulated_errors_count = count + 1
|
57
|
+
save_error_count(message_based_key, accumulated_errors_count)
|
58
|
+
else
|
59
|
+
backtrace_based_key =
|
60
|
+
"exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}"
|
61
|
+
|
62
|
+
if (count = error_grouping_cache.read(backtrace_based_key))
|
63
|
+
accumulated_errors_count = count + 1
|
64
|
+
save_error_count(backtrace_based_key, accumulated_errors_count)
|
65
|
+
else
|
66
|
+
save_error_count(backtrace_based_key, accumulated_errors_count)
|
67
|
+
save_error_count(message_based_key, accumulated_errors_count)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
options[:accumulated_errors_count] = accumulated_errors_count
|
72
|
+
end
|
73
|
+
|
74
|
+
def send_notification?(exception, count)
|
75
|
+
if notification_trigger.respond_to?(:call)
|
76
|
+
notification_trigger.call(exception, count)
|
77
|
+
else
|
78
|
+
factor = Math.log2(count)
|
79
|
+
factor.to_i == factor
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def log_cache_error(cache, exception, action)
|
86
|
+
"#{cache.inspect} failed to #{action}, reason: #{exception.message}. Falling back to memory cache store."
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/time"
|
4
|
+
require "action_dispatch"
|
5
|
+
|
6
|
+
module ExceptionNotifier
|
7
|
+
class Formatter
|
8
|
+
include ExceptionNotifier::BacktraceCleaner
|
9
|
+
|
10
|
+
attr_reader :app_name
|
11
|
+
|
12
|
+
def initialize(exception, opts = {})
|
13
|
+
@exception = exception
|
14
|
+
|
15
|
+
@env = opts[:env]
|
16
|
+
@errors_count = opts[:accumulated_errors_count].to_i
|
17
|
+
@app_name = opts[:app_name] || rails_app_name
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# :warning: Error occurred in production :warning:
|
22
|
+
# :warning: Error occurred :warning:
|
23
|
+
#
|
24
|
+
def title
|
25
|
+
env = Rails.env if defined?(::Rails) && ::Rails.respond_to?(:env)
|
26
|
+
|
27
|
+
if env
|
28
|
+
"⚠️ Error occurred in #{env} ⚠️"
|
29
|
+
else
|
30
|
+
"⚠️ Error occurred ⚠️"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# A *NoMethodError* occurred.
|
36
|
+
# 3 *NoMethodError* occurred.
|
37
|
+
# A *NoMethodError* occurred in *home#index*.
|
38
|
+
#
|
39
|
+
def subtitle
|
40
|
+
errors_text = if errors_count > 1
|
41
|
+
errors_count
|
42
|
+
else
|
43
|
+
/^[aeiou]/i.match?(exception.class.to_s) ? "An" : "A"
|
44
|
+
end
|
45
|
+
|
46
|
+
in_action = " in *#{controller_and_action}*" if controller
|
47
|
+
|
48
|
+
"#{errors_text} *#{exception.class}* occurred#{in_action}."
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
#
|
53
|
+
# *Request:*
|
54
|
+
# ```
|
55
|
+
# * url : https://www.example.com/
|
56
|
+
# * http_method : GET
|
57
|
+
# * ip_address : 127.0.0.1
|
58
|
+
# * parameters : {"controller"=>"home", "action"=>"index"}
|
59
|
+
# * timestamp : 2019-01-01 00:00:00 UTC
|
60
|
+
# ```
|
61
|
+
#
|
62
|
+
def request_message
|
63
|
+
request = ActionDispatch::Request.new(env) if env
|
64
|
+
return unless request
|
65
|
+
|
66
|
+
[
|
67
|
+
"```",
|
68
|
+
"* url : #{request.original_url}",
|
69
|
+
"* http_method : #{request.method}",
|
70
|
+
"* ip_address : #{request.remote_ip}",
|
71
|
+
"* parameters : #{request.filtered_parameters}",
|
72
|
+
"* timestamp : #{Time.current}",
|
73
|
+
"```"
|
74
|
+
].join("\n")
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
#
|
79
|
+
# *Backtrace:*
|
80
|
+
# ```
|
81
|
+
# * app/controllers/my_controller.rb:99:in `specific_function'
|
82
|
+
# * app/controllers/my_controller.rb:70:in `specific_param'
|
83
|
+
# * app/controllers/my_controller.rb:53:in `my_controller_params'
|
84
|
+
# ```
|
85
|
+
#
|
86
|
+
def backtrace_message
|
87
|
+
backtrace = exception.backtrace ? clean_backtrace(exception) : nil
|
88
|
+
|
89
|
+
return unless backtrace
|
90
|
+
|
91
|
+
text = []
|
92
|
+
|
93
|
+
text << "```"
|
94
|
+
backtrace.first(3).each { |line| text << "* #{line}" }
|
95
|
+
text << "```"
|
96
|
+
|
97
|
+
text.join("\n")
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# home#index
|
102
|
+
#
|
103
|
+
def controller_and_action
|
104
|
+
"#{controller.controller_name}##{controller.action_name}" if controller
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
attr_reader :exception, :env, :errors_count
|
110
|
+
|
111
|
+
def rails_app_name
|
112
|
+
return unless defined?(::Rails) && ::Rails.respond_to?(:application)
|
113
|
+
|
114
|
+
if Rails::VERSION::MAJOR >= 6
|
115
|
+
Rails.application.class.module_parent_name.underscore
|
116
|
+
else
|
117
|
+
Rails.application.class.parent_name.underscore
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def controller
|
122
|
+
env["action_controller.instance"] if env
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "active_support/core_ext/string/inflections"
|
5
|
+
require "active_support/core_ext/module/attribute_accessors"
|
6
|
+
require "exception_notifier/base_notifier"
|
7
|
+
require "exception_notifier/modules/error_grouping"
|
8
|
+
|
9
|
+
module ExceptionNotifier
|
10
|
+
include ErrorGrouping
|
11
|
+
|
12
|
+
autoload :BacktraceCleaner, "exception_notifier/modules/backtrace_cleaner"
|
13
|
+
autoload :Formatter, "exception_notifier/modules/formatter"
|
14
|
+
|
15
|
+
autoload :Notifier, "exception_notifier/notifier"
|
16
|
+
autoload :DbNotifier, "exception_notifier/db_notifier"
|
17
|
+
|
18
|
+
class UndefinedNotifierError < StandardError; end
|
19
|
+
|
20
|
+
# Define logger
|
21
|
+
mattr_accessor :logger
|
22
|
+
@@logger = Logger.new($stdout)
|
23
|
+
|
24
|
+
# Define a set of exceptions to be ignored, ie, dont send notifications when any of them are raised.
|
25
|
+
mattr_accessor :ignored_exceptions
|
26
|
+
@@ignored_exceptions = %w[
|
27
|
+
AbstractController::ActionNotFound
|
28
|
+
ActionController::BadRequest
|
29
|
+
ActionController::InvalidAuthenticityToken
|
30
|
+
ActionController::InvalidCrossOriginRequest
|
31
|
+
ActionController::ParameterMissing
|
32
|
+
ActionController::RoutingError
|
33
|
+
ActionController::UnknownFormat
|
34
|
+
ActionController::UrlGenerationError
|
35
|
+
ActionView::MissingTemplate
|
36
|
+
ActionView::TemplateError
|
37
|
+
ActiveRecord::RecordNotFound
|
38
|
+
Mime::Type::InvalidMimeType
|
39
|
+
Mongoid::Errors::DocumentNotFound
|
40
|
+
]
|
41
|
+
|
42
|
+
mattr_accessor :testing_mode
|
43
|
+
@@testing_mode = false
|
44
|
+
|
45
|
+
class << self
|
46
|
+
# Store conditions that decide when exceptions must be ignored or not.
|
47
|
+
@@ignores = []
|
48
|
+
|
49
|
+
# Store by-notifier conditions that decide when exceptions must be ignored or not.
|
50
|
+
@@by_notifier_ignores = {}
|
51
|
+
|
52
|
+
# Store notifiers that send notifications when exceptions are raised.
|
53
|
+
@@notifiers = {}
|
54
|
+
|
55
|
+
def testing_mode!
|
56
|
+
self.testing_mode = true
|
57
|
+
end
|
58
|
+
|
59
|
+
def notify_exception(exception, options = {}, &block)
|
60
|
+
return false if ignored_exception?(options[:ignore_exceptions], exception)
|
61
|
+
return false if ignored?(exception, options)
|
62
|
+
|
63
|
+
if error_grouping
|
64
|
+
errors_count = group_error!(exception, options)
|
65
|
+
return false unless send_notification?(exception, errors_count)
|
66
|
+
end
|
67
|
+
|
68
|
+
notification_fired = false
|
69
|
+
selected_notifiers = options.delete(:notifiers) || notifiers
|
70
|
+
[*selected_notifiers].each do |notifier|
|
71
|
+
unless notifier_ignored?(exception, options, notifier: notifier)
|
72
|
+
fire_notification(notifier, exception, options.dup, &block)
|
73
|
+
notification_fired = true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
notification_fired
|
78
|
+
end
|
79
|
+
|
80
|
+
def register_exception_notifier(name, notifier_or_options)
|
81
|
+
if notifier_or_options.respond_to?(:call)
|
82
|
+
@@notifiers[name] = notifier_or_options
|
83
|
+
elsif notifier_or_options.is_a?(Hash)
|
84
|
+
create_and_register_notifier(name, notifier_or_options)
|
85
|
+
else
|
86
|
+
raise ArgumentError, "Invalid notifier '#{name}' defined as #{notifier_or_options.inspect}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
alias_method :add_notifier, :register_exception_notifier
|
90
|
+
|
91
|
+
def unregister_exception_notifier(name)
|
92
|
+
@@notifiers.delete(name)
|
93
|
+
end
|
94
|
+
|
95
|
+
def registered_exception_notifier(name)
|
96
|
+
@@notifiers[name]
|
97
|
+
end
|
98
|
+
|
99
|
+
def notifiers
|
100
|
+
@@notifiers.keys
|
101
|
+
end
|
102
|
+
|
103
|
+
# Adds a condition to decide when an exception must be ignored or not.
|
104
|
+
#
|
105
|
+
# ExceptionNotifier.ignore_if do |exception, options|
|
106
|
+
# not Rails.env.production?
|
107
|
+
# end
|
108
|
+
def ignore_if(&block)
|
109
|
+
@@ignores << block
|
110
|
+
end
|
111
|
+
|
112
|
+
def ignore_notifier_if(notifier, &block)
|
113
|
+
@@by_notifier_ignores[notifier] = block
|
114
|
+
end
|
115
|
+
|
116
|
+
def ignore_crawlers(crawlers)
|
117
|
+
ignore_if do |_exception, opts|
|
118
|
+
opts.key?(:env) && from_crawler(opts[:env], crawlers)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def clear_ignore_conditions!
|
123
|
+
@@ignores.clear
|
124
|
+
@@by_notifier_ignores.clear
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def ignored?(exception, options)
|
130
|
+
@@ignores.any? { |condition| condition.call(exception, options) }
|
131
|
+
rescue Exception => e
|
132
|
+
raise e if @@testing_mode
|
133
|
+
|
134
|
+
logger.warn(
|
135
|
+
"An error occurred when evaluating an ignore condition. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
136
|
+
)
|
137
|
+
false
|
138
|
+
end
|
139
|
+
|
140
|
+
def notifier_ignored?(exception, options, notifier:)
|
141
|
+
return false unless @@by_notifier_ignores.key?(notifier)
|
142
|
+
|
143
|
+
condition = @@by_notifier_ignores[notifier]
|
144
|
+
condition.call(exception, options)
|
145
|
+
rescue Exception => e
|
146
|
+
raise e if @@testing_mode
|
147
|
+
|
148
|
+
logger.warn(<<~"MESSAGE")
|
149
|
+
An error occurred when evaluating a by-notifier ignore condition. #{e.class}: #{e.message}
|
150
|
+
#{e.backtrace.join("\n")}
|
151
|
+
MESSAGE
|
152
|
+
false
|
153
|
+
end
|
154
|
+
|
155
|
+
def ignored_exception?(ignore_array, exception)
|
156
|
+
all_ignored_exceptions = (Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s)
|
157
|
+
exception_ancestors = exception.singleton_class.ancestors.map(&:to_s)
|
158
|
+
!(all_ignored_exceptions & exception_ancestors).empty?
|
159
|
+
end
|
160
|
+
|
161
|
+
def fire_notification(notifier_name, exception, options, &block)
|
162
|
+
notifier = registered_exception_notifier(notifier_name)
|
163
|
+
notifier.call(exception, options, &block)
|
164
|
+
rescue Exception => e
|
165
|
+
raise e if @@testing_mode
|
166
|
+
|
167
|
+
logger.warn(
|
168
|
+
"An error occurred when sending a notification using '#{notifier_name}' notifier." \
|
169
|
+
"#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
170
|
+
)
|
171
|
+
false
|
172
|
+
end
|
173
|
+
|
174
|
+
def create_and_register_notifier(name, options)
|
175
|
+
notifier_classname = "#{name}_notifier".camelize
|
176
|
+
notifier_class = ExceptionNotifier.const_get(notifier_classname)
|
177
|
+
notifier = notifier_class.new(options)
|
178
|
+
register_exception_notifier(name, notifier)
|
179
|
+
rescue NameError => e
|
180
|
+
raise UndefinedNotifierError,
|
181
|
+
"No notifier named '#{name}' was found. Please, revise your configuration options. Cause: #{e.message}"
|
182
|
+
end
|
183
|
+
|
184
|
+
def from_crawler(env, ignored_crawlers)
|
185
|
+
agent = env["HTTP_USER_AGENT"]
|
186
|
+
Array(ignored_crawlers).any? do |crawler|
|
187
|
+
agent =~ Regexp.new(crawler)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
metadata
CHANGED
@@ -1,57 +1,127 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: exception-track
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jason Lee
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-12-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: kaminari
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '0.15'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '0.15'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: rails
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '5.2'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '5.2'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: pg
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
48
|
-
type: :
|
47
|
+
version: '1'
|
48
|
+
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mocha
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.13.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.13.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mock_redis
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.19.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.19.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: resque
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.8.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.8.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sidekiq
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 5.0.4
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 5.0.4
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: timecop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.9.0
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.9.0
|
55
125
|
description: Tracking exceptions for Rails application store them in database by exception_notification
|
56
126
|
gem.
|
57
127
|
email:
|
@@ -77,7 +147,18 @@ files:
|
|
77
147
|
- lib/exception-track/engine.rb
|
78
148
|
- lib/exception-track/log_subscriber.rb
|
79
149
|
- lib/exception-track/version.rb
|
80
|
-
- lib/
|
150
|
+
- lib/exception_notification.rb
|
151
|
+
- lib/exception_notification/rack.rb
|
152
|
+
- lib/exception_notification/rails.rb
|
153
|
+
- lib/exception_notification/resque.rb
|
154
|
+
- lib/exception_notification/sidekiq.rb
|
155
|
+
- lib/exception_notification/version.rb
|
156
|
+
- lib/exception_notifier/base_notifier.rb
|
157
|
+
- lib/exception_notifier/db_notifier.rb
|
158
|
+
- lib/exception_notifier/modules/backtrace_cleaner.rb
|
159
|
+
- lib/exception_notifier/modules/error_grouping.rb
|
160
|
+
- lib/exception_notifier/modules/formatter.rb
|
161
|
+
- lib/exception_notifier/notifier.rb
|
81
162
|
- lib/generators/exception_track/install_generator.rb
|
82
163
|
homepage: https://github.com/rails-engine/exception-track
|
83
164
|
licenses:
|
@@ -98,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
179
|
- !ruby/object:Gem::Version
|
99
180
|
version: '0'
|
100
181
|
requirements: []
|
101
|
-
rubygems_version: 3.
|
182
|
+
rubygems_version: 3.2.3
|
102
183
|
signing_key:
|
103
184
|
specification_version: 4
|
104
185
|
summary: Tracking exceptions for Rails application store them in database.
|