exception-track 1.2.0 → 1.3.0
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 +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.
|