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,118 @@
1
+ require 'active_support/core_ext/time'
2
+ require 'action_dispatch'
3
+
4
+ module ExceptionNotifier
5
+ class Formatter
6
+ include ExceptionNotifier::BacktraceCleaner
7
+
8
+ attr_reader :app_name
9
+
10
+ def initialize(exception, opts = {})
11
+ @exception = exception
12
+
13
+ @env = opts[:env]
14
+ @errors_count = opts[:accumulated_errors_count].to_i
15
+ @app_name = opts[:app_name] || rails_app_name
16
+ end
17
+
18
+ #
19
+ # :warning: Error occurred in production :warning:
20
+ # :warning: Error occurred :warning:
21
+ #
22
+ def title
23
+ env = Rails.env if defined?(::Rails) && ::Rails.respond_to?(:env)
24
+
25
+ if env
26
+ "⚠️ Error occurred in #{env} ⚠️"
27
+ else
28
+ '⚠️ Error occurred ⚠️'
29
+ end
30
+ end
31
+
32
+ #
33
+ # A *NoMethodError* occurred.
34
+ # 3 *NoMethodError* occurred.
35
+ # A *NoMethodError* occurred in *home#index*.
36
+ #
37
+ def subtitle
38
+ errors_text = if errors_count > 1
39
+ errors_count
40
+ else
41
+ exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A'
42
+ end
43
+
44
+ in_action = " in *#{controller_and_action}*" if controller
45
+
46
+ "#{errors_text} *#{exception.class}* occurred#{in_action}."
47
+ end
48
+
49
+ #
50
+ #
51
+ # *Request:*
52
+ # ```
53
+ # * url : https://www.example.com/
54
+ # * http_method : GET
55
+ # * ip_address : 127.0.0.1
56
+ # * parameters : {"controller"=>"home", "action"=>"index"}
57
+ # * timestamp : 2019-01-01 00:00:00 UTC
58
+ # ```
59
+ #
60
+ def request_message
61
+ request = ActionDispatch::Request.new(env) if env
62
+ return unless request
63
+
64
+ [
65
+ '```',
66
+ "* url : #{request.original_url}",
67
+ "* http_method : #{request.method}",
68
+ "* ip_address : #{request.remote_ip}",
69
+ "* parameters : #{request.filtered_parameters}",
70
+ "* timestamp : #{Time.current}",
71
+ '```'
72
+ ].join("\n")
73
+ end
74
+
75
+ #
76
+ #
77
+ # *Backtrace:*
78
+ # ```
79
+ # * app/controllers/my_controller.rb:99:in `specific_function'
80
+ # * app/controllers/my_controller.rb:70:in `specific_param'
81
+ # * app/controllers/my_controller.rb:53:in `my_controller_params'
82
+ # ```
83
+ #
84
+ def backtrace_message
85
+ backtrace = exception.backtrace ? clean_backtrace(exception) : nil
86
+
87
+ return unless backtrace
88
+
89
+ text = []
90
+
91
+ text << '```'
92
+ backtrace.first(3).each { |line| text << "* #{line}" }
93
+ text << '```'
94
+
95
+ text.join("\n")
96
+ end
97
+
98
+ #
99
+ # home#index
100
+ #
101
+ def controller_and_action
102
+ "#{controller.controller_name}##{controller.action_name}" if controller
103
+ end
104
+
105
+ private
106
+
107
+ attr_reader :exception, :env, :errors_count
108
+
109
+ def rails_app_name
110
+ return unless defined?(::Rails) && ::Rails.respond_to?(:application)
111
+ Rails.application.class.parent_name.underscore
112
+ end
113
+
114
+ def controller
115
+ env['action_controller.instance'] if env
116
+ end
117
+ end
118
+ end
@@ -1,185 +1,15 @@
1
- require 'action_mailer'
2
- require 'pp'
1
+ require 'active_support/deprecation'
3
2
 
4
- class ExceptionNotifier
5
- class Notifier < ActionMailer::Base
6
- self.mailer_name = 'exception_notifier'
7
-
8
- #Append application view path to the ExceptionNotifier lookup context.
9
- self.append_view_path "#{File.dirname(__FILE__)}/views"
10
-
11
- class << self
12
- attr_writer :default_sender_address
13
- attr_writer :default_exception_recipients
14
- attr_writer :default_email_prefix
15
- attr_writer :default_email_format
16
- attr_writer :default_sections
17
- attr_writer :default_background_sections
18
- attr_writer :default_verbose_subject
19
- attr_writer :default_normalize_subject
20
- attr_writer :default_smtp_settings
21
- attr_writer :default_email_headers
22
-
23
- def default_sender_address
24
- @default_sender_address || %("Exception Notifier" <exception.notifier@default.com>)
25
- end
26
-
27
- def default_exception_recipients
28
- @default_exception_recipients || []
29
- end
30
-
31
- def default_email_prefix
32
- @default_email_prefix || "[ERROR] "
33
- end
34
-
35
- def default_email_format
36
- @default_email_format || :text
37
- end
38
-
39
- def default_sections
40
- @default_sections || %w(request session environment backtrace)
41
- end
42
-
43
- def default_background_sections
44
- @default_background_sections || %w(backtrace data)
45
- end
46
-
47
- def default_verbose_subject
48
- @default_verbose_subject.nil? || @default_verbose_subject
49
- end
50
-
51
- def default_normalize_subject
52
- @default_normalize_prefix || false
53
- end
54
-
55
- def default_smtp_settings
56
- @default_smtp_settings || nil
57
- end
58
-
59
- def default_email_headers
60
- @default_email_headers || {}
61
- end
62
-
63
- def default_options
64
- { :sender_address => default_sender_address,
65
- :exception_recipients => default_exception_recipients,
66
- :email_prefix => default_email_prefix,
67
- :email_format => default_email_format,
68
- :sections => default_sections,
69
- :background_sections => default_background_sections,
70
- :verbose_subject => default_verbose_subject,
71
- :normalize_subject => default_normalize_subject,
72
- :template_path => mailer_name,
73
- :smtp_settings => default_smtp_settings,
74
- :email_headers => default_email_headers }
75
- end
76
-
77
- def normalize_digits(string)
78
- string.gsub(/[0-9]+/, 'N')
79
- end
80
- end
81
-
82
- class MissingController
83
- def method_missing(*args, &block)
84
- end
85
- end
86
-
87
- def exception_notification(env, exception, options={})
88
- load_custom_views
89
-
90
- @env = env
91
- @exception = exception
92
- @options = options.reverse_merge(env['exception_notifier.options'] || {}).reverse_merge(self.class.default_options)
93
- @kontroller = env['action_controller.instance'] || MissingController.new
94
- @request = ActionDispatch::Request.new(env)
95
- @backtrace = exception.backtrace ? clean_backtrace(exception) : []
96
- @sections = @options[:sections]
97
- @data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
98
- @sections = @sections + %w(data) unless @data.empty?
99
-
100
- compose_email
101
- end
102
-
103
- def background_exception_notification(exception, options={})
104
- load_custom_views
105
-
106
- if @notifier = Rails.application.config.middleware.detect{ |x| x.klass == ExceptionNotifier }
107
- @options = options.reverse_merge(@notifier.args.first || {}).reverse_merge(self.class.default_options)
108
- @exception = exception
109
- @backtrace = exception.backtrace || []
110
- @sections = @options[:background_sections]
111
- @data = options[:data] || {}
112
-
113
- compose_email
114
- end
115
- end
116
-
117
- private
118
-
119
- def compose_subject
120
- subject = "#{@options[:email_prefix]}"
121
- subject << "#{@kontroller.controller_name}##{@kontroller.action_name}" if @kontroller
122
- subject << " (#{@exception.class})"
123
- subject << " #{@exception.message.inspect}" if @options[:verbose_subject]
124
- subject = normalize_digits(subject) if @options[:normalize_subject]
125
- subject.length > 120 ? subject[0...120] + "..." : subject
126
- end
127
-
128
- def set_data_variables
129
- @data.each do |name, value|
130
- instance_variable_set("@#{name}", value)
131
- end
132
- end
133
-
134
- def clean_backtrace(exception)
135
- if Rails.respond_to?(:backtrace_cleaner)
136
- Rails.backtrace_cleaner.send(:filter, exception.backtrace)
137
- else
138
- exception.backtrace
139
- end
140
- end
141
-
142
- helper_method :inspect_object
143
-
144
- def inspect_object(object)
145
- case object
146
- when Hash, Array
147
- object.inspect
148
- when ActionController::Base
149
- "#{object.controller_name}##{object.action_name}"
150
- else
151
- object.to_s
152
- end
153
- end
154
-
155
- def html_mail?
156
- @options[:email_format] == :html
157
- end
158
-
159
- def compose_email
160
- set_data_variables
161
- subject = compose_subject
162
- name = @env.nil? ? 'background_exception_notification' : 'exception_notification'
163
-
164
- headers = {
165
- :to => @options[:exception_recipients],
166
- :from => @options[:sender_address],
167
- :subject => subject,
168
- :template_name => name
169
- }.merge(@options[:email_headers])
170
-
171
- mail = mail(headers) do |format|
172
- format.text
173
- format.html if html_mail?
174
- end
175
-
176
- mail.delivery_method.settings.merge!(@options[:smtp_settings]) if @options[:smtp_settings]
177
-
178
- mail
3
+ module ExceptionNotifier
4
+ class Notifier
5
+ def self.exception_notification(env, exception, options = {})
6
+ ActiveSupport::Deprecation.warn 'Please use ExceptionNotifier.notify_exception(exception, options.merge(env: env)).'
7
+ ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options.merge(env: env))
179
8
  end
180
9
 
181
- def load_custom_views
182
- self.prepend_view_path Rails.root.nil? ? "app/views" : "#{Rails.root}/app/views" if defined?(Rails)
10
+ def self.background_exception_notification(exception, options = {})
11
+ ActiveSupport::Deprecation.warn 'Please use ExceptionNotifier.notify_exception(exception, options).'
12
+ ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options)
183
13
  end
184
14
  end
185
15
  end
@@ -0,0 +1,111 @@
1
+ module ExceptionNotifier
2
+ class SlackNotifier < BaseNotifier
3
+ include ExceptionNotifier::BacktraceCleaner
4
+
5
+ attr_accessor :notifier
6
+
7
+ def initialize(options)
8
+ super
9
+ begin
10
+ @ignore_data_if = options[:ignore_data_if]
11
+ @backtrace_lines = options.fetch(:backtrace_lines, 10)
12
+ @additional_fields = options[:additional_fields]
13
+
14
+ webhook_url = options.fetch(:webhook_url)
15
+ @message_opts = options.fetch(:additional_parameters, {})
16
+ @color = @message_opts.delete(:color) { 'danger' }
17
+ @notifier = Slack::Notifier.new webhook_url, options
18
+ rescue StandardError
19
+ @notifier = nil
20
+ end
21
+ end
22
+
23
+ def call(exception, options = {})
24
+ clean_message = exception.message.tr('`', "'")
25
+ attchs = attchs(exception, clean_message, options)
26
+
27
+ return unless valid?
28
+
29
+ args = [exception, options, clean_message, @message_opts.merge(attachments: attchs)]
30
+ send_notice(*args) do |_msg, message_opts|
31
+ message_opts[:channel] = options[:channel] if options.key?(:channel)
32
+
33
+ @notifier.ping '', message_opts
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ def valid?
40
+ !@notifier.nil?
41
+ end
42
+
43
+ def deep_reject(hash, block)
44
+ hash.each do |k, v|
45
+ deep_reject(v, block) if v.is_a?(Hash)
46
+
47
+ hash.delete(k) if block.call(k, v)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def attchs(exception, clean_message, options)
54
+ text, data = information_from_options(exception.class, options)
55
+ backtrace = clean_backtrace(exception) if exception.backtrace
56
+ fields = fields(clean_message, backtrace, data)
57
+
58
+ [color: @color, text: text, fields: fields, mrkdwn_in: %w[text fields]]
59
+ end
60
+
61
+ def information_from_options(exception_class, options)
62
+ errors_count = options[:accumulated_errors_count].to_i
63
+
64
+ measure_word = if errors_count > 1
65
+ errors_count
66
+ else
67
+ exception_class.to_s =~ /^[aeiou]/i ? 'An' : 'A'
68
+ end
69
+
70
+ exception_name = "*#{measure_word}* `#{exception_class}`"
71
+ env = options[:env]
72
+
73
+ if env.nil?
74
+ data = options[:data] || {}
75
+ text = "#{exception_name} *occured in background*\n"
76
+ else
77
+ data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
78
+
79
+ kontroller = env['action_controller.instance']
80
+ request = "#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>"
81
+ text = "#{exception_name} *occurred while* `#{request}`"
82
+ text += " *was processed by* `#{kontroller.controller_name}##{kontroller.action_name}`" if kontroller
83
+ text += "\n"
84
+ end
85
+
86
+ [text, data]
87
+ end
88
+
89
+ def fields(clean_message, backtrace, data)
90
+ fields = [
91
+ { title: 'Exception', value: clean_message },
92
+ { title: 'Hostname', value: Socket.gethostname }
93
+ ]
94
+
95
+ if backtrace
96
+ formatted_backtrace = "```#{backtrace.first(@backtrace_lines).join("\n")}```"
97
+ fields << { title: 'Backtrace', value: formatted_backtrace }
98
+ end
99
+
100
+ unless data.empty?
101
+ deep_reject(data, @ignore_data_if) if @ignore_data_if.is_a?(Proc)
102
+ data_string = data.map { |k, v| "#{k}: #{v}" }.join("\n")
103
+ fields << { title: 'Data', value: "```#{data_string}```" }
104
+ end
105
+
106
+ fields.concat(@additional_fields) if @additional_fields
107
+
108
+ fields
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,85 @@
1
+ module ExceptionNotifier
2
+ class SnsNotifier < BaseNotifier
3
+ def initialize(options)
4
+ super
5
+
6
+ raise ArgumentError, "You must provide 'region' option" unless options[:region]
7
+ raise ArgumentError, "You must provide 'access_key_id' option" unless options[:access_key_id]
8
+ raise ArgumentError, "You must provide 'secret_access_key' option" unless options[:secret_access_key]
9
+
10
+ @notifier = Aws::SNS::Client.new(
11
+ region: options[:region],
12
+ access_key_id: options[:access_key_id],
13
+ secret_access_key: options[:secret_access_key]
14
+ )
15
+ @options = default_options.merge(options)
16
+ end
17
+
18
+ def call(exception, custom_opts = {})
19
+ custom_options = options.merge(custom_opts)
20
+
21
+ subject = build_subject(exception, custom_options)
22
+ message = build_message(exception, custom_options)
23
+
24
+ notifier.publish(
25
+ topic_arn: custom_options[:topic_arn],
26
+ message: message,
27
+ subject: subject
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :notifier, :options
34
+
35
+ def build_subject(exception, options)
36
+ subject = "#{options[:sns_prefix]} - "
37
+ subject << accumulated_exception_name(exception, options)
38
+ subject << ' occurred'
39
+ subject.length > 120 ? subject[0...120] + '...' : subject
40
+ end
41
+
42
+ def build_message(exception, options)
43
+ exception_name = accumulated_exception_name(exception, options)
44
+
45
+ if options[:env].nil?
46
+ text = "#{exception_name} occured in background\n"
47
+ else
48
+ env = options[:env]
49
+
50
+ kontroller = env['action_controller.instance']
51
+ request = "#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>"
52
+
53
+ text = "#{exception_name} occurred while #{request}"
54
+ text += " was processed by #{kontroller.controller_name}##{kontroller.action_name}\n" if kontroller
55
+ end
56
+
57
+ text += "Exception: #{exception.message}\n"
58
+ text += "Hostname: #{Socket.gethostname}\n"
59
+
60
+ return unless exception.backtrace
61
+
62
+ formatted_backtrace = exception.backtrace.first(options[:backtrace_lines]).join("\n").to_s
63
+ text + "Backtrace:\n#{formatted_backtrace}\n"
64
+ end
65
+
66
+ def accumulated_exception_name(exception, options)
67
+ errors_count = options[:accumulated_errors_count].to_i
68
+
69
+ measure_word = if errors_count > 1
70
+ errors_count
71
+ else
72
+ exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A'
73
+ end
74
+
75
+ "#{measure_word} #{exception.class}"
76
+ end
77
+
78
+ def default_options
79
+ {
80
+ sns_prefix: '[ERROR]',
81
+ backtrace_lines: 10
82
+ }
83
+ end
84
+ end
85
+ end