exception_notification 3.0.1 → 4.4.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.
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