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,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