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.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/Appraisals +7 -0
  3. data/CHANGELOG.rdoc +129 -1
  4. data/CODE_OF_CONDUCT.md +22 -0
  5. data/CONTRIBUTING.md +29 -1
  6. data/Gemfile +1 -1
  7. data/MIT-LICENSE +23 -0
  8. data/README.md +168 -222
  9. data/Rakefile +5 -11
  10. data/docs/notifiers/campfire.md +50 -0
  11. data/docs/notifiers/custom.md +42 -0
  12. data/docs/notifiers/datadog.md +51 -0
  13. data/docs/notifiers/email.md +195 -0
  14. data/docs/notifiers/google_chat.md +31 -0
  15. data/docs/notifiers/hipchat.md +66 -0
  16. data/docs/notifiers/irc.md +97 -0
  17. data/docs/notifiers/mattermost.md +115 -0
  18. data/docs/notifiers/slack.md +161 -0
  19. data/docs/notifiers/sns.md +37 -0
  20. data/docs/notifiers/teams.md +54 -0
  21. data/docs/notifiers/webhook.md +60 -0
  22. data/examples/sample_app.rb +54 -0
  23. data/examples/sinatra/Gemfile +8 -0
  24. data/examples/sinatra/Gemfile.lock +95 -0
  25. data/examples/sinatra/Procfile +2 -0
  26. data/examples/sinatra/README.md +11 -0
  27. data/examples/sinatra/config.ru +3 -0
  28. data/examples/sinatra/sinatra_app.rb +36 -0
  29. data/exception_notification.gemspec +32 -11
  30. data/gemfiles/rails4_0.gemfile +7 -0
  31. data/gemfiles/rails4_1.gemfile +7 -0
  32. data/gemfiles/rails4_2.gemfile +7 -0
  33. data/gemfiles/rails5_0.gemfile +7 -0
  34. data/gemfiles/rails5_1.gemfile +7 -0
  35. data/gemfiles/rails5_2.gemfile +7 -0
  36. data/gemfiles/rails6_0.gemfile +7 -0
  37. data/lib/exception_notification.rb +11 -0
  38. data/lib/exception_notification/rack.rb +55 -0
  39. data/lib/exception_notification/rails.rb +9 -0
  40. data/lib/exception_notification/resque.rb +22 -0
  41. data/lib/exception_notification/sidekiq.rb +27 -0
  42. data/lib/exception_notification/version.rb +3 -0
  43. data/lib/exception_notifier.rb +137 -61
  44. data/lib/exception_notifier/base_notifier.rb +24 -0
  45. data/lib/exception_notifier/campfire_notifier.rb +16 -11
  46. data/lib/exception_notifier/datadog_notifier.rb +153 -0
  47. data/lib/exception_notifier/email_notifier.rb +196 -0
  48. data/lib/exception_notifier/google_chat_notifier.rb +42 -0
  49. data/lib/exception_notifier/hipchat_notifier.rb +49 -0
  50. data/lib/exception_notifier/irc_notifier.rb +57 -0
  51. data/lib/exception_notifier/mattermost_notifier.rb +72 -0
  52. data/lib/exception_notifier/modules/backtrace_cleaner.rb +11 -0
  53. data/lib/exception_notifier/modules/error_grouping.rb +77 -0
  54. data/lib/exception_notifier/modules/formatter.rb +118 -0
  55. data/lib/exception_notifier/notifier.rb +9 -179
  56. data/lib/exception_notifier/slack_notifier.rb +111 -0
  57. data/lib/exception_notifier/sns_notifier.rb +85 -0
  58. data/lib/exception_notifier/teams_notifier.rb +193 -0
  59. data/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +3 -1
  60. data/lib/exception_notifier/views/exception_notifier/_data.html.erb +6 -1
  61. data/lib/exception_notifier/views/exception_notifier/_environment.html.erb +8 -6
  62. data/lib/exception_notifier/views/exception_notifier/_environment.text.erb +1 -4
  63. data/lib/exception_notifier/views/exception_notifier/_request.html.erb +36 -5
  64. data/lib/exception_notifier/views/exception_notifier/_request.text.erb +10 -5
  65. data/lib/exception_notifier/views/exception_notifier/_session.html.erb +10 -2
  66. data/lib/exception_notifier/views/exception_notifier/_session.text.erb +2 -2
  67. data/lib/exception_notifier/views/exception_notifier/_title.html.erb +3 -3
  68. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.html.erb +38 -11
  69. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +10 -11
  70. data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +38 -22
  71. data/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +2 -3
  72. data/lib/exception_notifier/webhook_notifier.rb +51 -0
  73. data/lib/generators/exception_notification/install_generator.rb +15 -0
  74. data/lib/generators/exception_notification/templates/exception_notification.rb.erb +55 -0
  75. data/test/exception_notification/rack_test.rb +60 -0
  76. data/test/exception_notification/resque_test.rb +52 -0
  77. data/test/exception_notifier/campfire_notifier_test.rb +120 -0
  78. data/test/exception_notifier/datadog_notifier_test.rb +151 -0
  79. data/test/exception_notifier/email_notifier_test.rb +351 -0
  80. data/test/exception_notifier/google_chat_notifier_test.rb +181 -0
  81. data/test/exception_notifier/hipchat_notifier_test.rb +218 -0
  82. data/test/exception_notifier/irc_notifier_test.rb +137 -0
  83. data/test/exception_notifier/mattermost_notifier_test.rb +202 -0
  84. data/test/exception_notifier/modules/error_grouping_test.rb +165 -0
  85. data/test/exception_notifier/modules/formatter_test.rb +150 -0
  86. data/test/exception_notifier/sidekiq_test.rb +38 -0
  87. data/test/exception_notifier/slack_notifier_test.rb +227 -0
  88. data/test/exception_notifier/sns_notifier_test.rb +121 -0
  89. data/test/exception_notifier/teams_notifier_test.rb +90 -0
  90. data/test/exception_notifier/webhook_notifier_test.rb +96 -0
  91. data/test/exception_notifier_test.rb +182 -0
  92. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.html.erb +0 -0
  93. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.text.erb +0 -0
  94. data/test/{dummy/app → support}/views/exception_notifier/_new_section.html.erb +0 -0
  95. data/test/{dummy/app → support}/views/exception_notifier/_new_section.text.erb +0 -0
  96. data/test/test_helper.rb +12 -8
  97. metadata +333 -164
  98. data/.gemtest +0 -0
  99. data/Gemfile.lock +0 -122
  100. data/test/background_exception_notification_test.rb +0 -82
  101. data/test/campfire_test.rb +0 -53
  102. data/test/dummy/.gitignore +0 -4
  103. data/test/dummy/Gemfile +0 -33
  104. data/test/dummy/Gemfile.lock +0 -118
  105. data/test/dummy/Rakefile +0 -7
  106. data/test/dummy/app/controllers/application_controller.rb +0 -3
  107. data/test/dummy/app/controllers/posts_controller.rb +0 -30
  108. data/test/dummy/app/helpers/application_helper.rb +0 -2
  109. data/test/dummy/app/helpers/posts_helper.rb +0 -2
  110. data/test/dummy/app/models/post.rb +0 -2
  111. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  112. data/test/dummy/app/views/posts/_form.html.erb +0 -0
  113. data/test/dummy/app/views/posts/new.html.erb +0 -0
  114. data/test/dummy/app/views/posts/show.html.erb +0 -0
  115. data/test/dummy/config.ru +0 -4
  116. data/test/dummy/config/application.rb +0 -42
  117. data/test/dummy/config/boot.rb +0 -6
  118. data/test/dummy/config/database.yml +0 -22
  119. data/test/dummy/config/environment.rb +0 -13
  120. data/test/dummy/config/environments/development.rb +0 -24
  121. data/test/dummy/config/environments/production.rb +0 -49
  122. data/test/dummy/config/environments/test.rb +0 -35
  123. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  124. data/test/dummy/config/initializers/inflections.rb +0 -10
  125. data/test/dummy/config/initializers/mime_types.rb +0 -5
  126. data/test/dummy/config/initializers/secret_token.rb +0 -7
  127. data/test/dummy/config/initializers/session_store.rb +0 -8
  128. data/test/dummy/config/locales/en.yml +0 -5
  129. data/test/dummy/config/routes.rb +0 -3
  130. data/test/dummy/db/migrate/20110729022608_create_posts.rb +0 -15
  131. data/test/dummy/db/schema.rb +0 -24
  132. data/test/dummy/db/seeds.rb +0 -7
  133. data/test/dummy/lib/tasks/.gitkeep +0 -0
  134. data/test/dummy/public/404.html +0 -26
  135. data/test/dummy/public/422.html +0 -26
  136. data/test/dummy/public/500.html +0 -26
  137. data/test/dummy/public/favicon.ico +0 -0
  138. data/test/dummy/public/images/rails.png +0 -0
  139. data/test/dummy/public/index.html +0 -239
  140. data/test/dummy/public/javascripts/application.js +0 -2
  141. data/test/dummy/public/javascripts/controls.js +0 -965
  142. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  143. data/test/dummy/public/javascripts/effects.js +0 -1123
  144. data/test/dummy/public/javascripts/prototype.js +0 -6001
  145. data/test/dummy/public/javascripts/rails.js +0 -191
  146. data/test/dummy/public/robots.txt +0 -5
  147. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  148. data/test/dummy/public/stylesheets/scaffold.css +0 -56
  149. data/test/dummy/script/rails +0 -6
  150. data/test/dummy/test/fixtures/posts.yml +0 -11
  151. data/test/dummy/test/functional/posts_controller_test.rb +0 -239
  152. data/test/dummy/test/test_helper.rb +0 -13
  153. 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
@@ -0,0 +1,3 @@
1
+ module ExceptionNotification
2
+ VERSION = '4.4.0'.freeze
3
+ end
@@ -1,75 +1,151 @@
1
- require 'action_dispatch'
2
- require 'exception_notifier/notifier'
3
- require 'exception_notifier/campfire_notifier'
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
- class ExceptionNotifier
7
+ module ExceptionNotifier
8
+ include ErrorGrouping
6
9
 
7
- def self.default_ignore_exceptions
8
- [].tap do |exceptions|
9
- exceptions << 'ActiveRecord::RecordNotFound'
10
- exceptions << 'AbstractController::ActionNotFound'
11
- exceptions << 'ActionController::RoutingError'
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
- def self.default_ignore_crawlers
16
- []
17
- end
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
- def initialize(app, options = {})
20
- @app, @options = app, options
21
-
22
- Notifier.default_sender_address = @options[:sender_address]
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
- def call(env)
41
- @app.call(env)
42
- rescue Exception => exception
43
- options = (env['exception_notifier.options'] ||= Notifier.default_options)
44
- options.reverse_merge!(@options)
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
- raise exception
55
- end
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
- private
77
+ def unregister_exception_notifier(name)
78
+ @@notifiers.delete(name)
79
+ end
58
80
 
59
- def ignored_exception(ignore_array, exception)
60
- Array.wrap(ignore_array).map(&:to_s).include?(exception.class.name)
61
- end
81
+ def registered_exception_notifier(name)
82
+ @@notifiers[name]
83
+ end
62
84
 
63
- def from_crawler(ignore_array, agent)
64
- ignore_array.each do |crawler|
65
- return true if (agent =~ Regexp.new(crawler))
66
- end unless ignore_array.blank?
67
- false
68
- end
85
+ def notifiers
86
+ @@notifiers.keys
87
+ end
69
88
 
70
- def conditionally_ignored(ignore_proc, env, exception)
71
- ignore_proc.call(env, exception)
72
- rescue Exception => ex
73
- false
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
- class ExceptionNotifier
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 exception_notification(exception)
23
- @room.paste "A new exception occurred: '#{exception.message}' on '#{exception.backtrace.first}'" if active?
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