Module: ExceptionHandling

Defined in:
lib/exception_handling.rb,
lib/exception_handling/testing.rb,
lib/exception_handling/version.rb,
lib/exception_handling/exception_info.rb,
lib/exception_handling/logging_methods.rb,
lib/exception_handling/escalate_callback.rb,
lib/exception_handling/exception_catalog.rb,
lib/exception_handling/exception_description.rb

Overview

some useful test objects

Defined Under Namespace

Modules: EscalateCallback, LoggingMethods, Testing Classes: ClientLoggingError, ExceptionCatalog, ExceptionDescription, ExceptionInfo, Warning

Constant Summary collapse

SUMMARY_THRESHOLD =
5
SUMMARY_PERIOD =

1.hour

60 * 60
AUTHENTICATION_HEADERS =
['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION', 'REDIRECT_X_HTTP_AUTHORIZATION'].freeze
HONEYBADGER_STATUSES =
[:success, :failure, :skipped].freeze
VERSION =
'3.1.1'

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.current_controllerObject

internal settings (don’t set directly)



122
123
124
# File 'lib/exception_handling.rb', line 122

def current_controller
  @current_controller
end

.custom_data_hookObject

Returns the value of attribute custom_data_hook.



77
78
79
# File 'lib/exception_handling.rb', line 77

def custom_data_hook
  @custom_data_hook
end

.environmentObject

Returns the value of attribute environment.



76
77
78
# File 'lib/exception_handling.rb', line 76

def environment
  @environment
end

.filter_list_filenameObject

Returns the value of attribute filter_list_filename.



81
82
83
# File 'lib/exception_handling.rb', line 81

def filter_list_filename
  @filter_list_filename
end

.honeybadger_auto_taggerObject

Returns the value of attribute honeybadger_auto_tagger.



82
83
84
# File 'lib/exception_handling.rb', line 82

def honeybadger_auto_tagger
  @honeybadger_auto_tagger
end

.honeybadger_log_context_tagsObject (readonly)

Returns the value of attribute honeybadger_log_context_tags.



83
84
85
# File 'lib/exception_handling.rb', line 83

def honeybadger_log_context_tags
  @honeybadger_log_context_tags
end

.last_exception_timestampObject

Returns the value of attribute last_exception_timestamp.



123
124
125
# File 'lib/exception_handling.rb', line 123

def last_exception_timestamp
  @last_exception_timestamp
end

.periodic_exception_intervalsObject

Returns the value of attribute periodic_exception_intervals.



124
125
126
# File 'lib/exception_handling.rb', line 124

def periodic_exception_intervals
  @periodic_exception_intervals
end

.post_log_error_hookObject

Returns the value of attribute post_log_error_hook.



78
79
80
# File 'lib/exception_handling.rb', line 78

def post_log_error_hook
  @post_log_error_hook
end

.production_support_recipientsObject

optional settings



75
76
77
# File 'lib/exception_handling.rb', line 75

def production_support_recipients
  @production_support_recipients
end

.server_nameObject



40
41
42
# File 'lib/exception_handling.rb', line 40

def server_name
  @server_name or raise ArgumentError, "You must assign a value to #{name}.server_name"
end

.stub_handlerObject

Returns the value of attribute stub_handler.



79
80
81
# File 'lib/exception_handling.rb', line 79

def stub_handler
  @stub_handler
end

Class Method Details

.add_honeybadger_tag_from_log_context(tag_name, path:) ⇒ Object

Parameters:

  • tag_name (String)
  • path (Array<String>)


106
107
108
109
110
111
112
113
# File 'lib/exception_handling.rb', line 106

def add_honeybadger_tag_from_log_context(tag_name, path:)
  tag_name.is_a?(String) or raise ArgumentError, "tag_name must be a String, #{tag_name.inspect}"
  (path.is_a?(Array) && path.all? { _1.is_a?(String) }) or raise ArgumentError, "path must be an Array<String>, #{path.inspect}"
  if @honeybadger_log_context_tags.key?(tag_name)
    log_warning("Overwriting existing tag path for #{tag_name.inspect} from #{@honeybadger_log_context_tags[tag_name]} to #{path}")
  end
  @honeybadger_log_context_tags[tag_name] = path
end

.clean_backtrace(exception) ⇒ Object



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/exception_handling.rb', line 334

def clean_backtrace(exception)
  backtrace = if exception.backtrace.nil?
                ['<no backtrace>']
              elsif exception.is_a?(ClientLoggingError)
                exception.backtrace
              elsif defined?(Rails) && defined?(Rails.backtrace_cleaner)
                Rails.backtrace_cleaner.clean(exception.backtrace)
              else
                exception.backtrace
  end

  # The rails backtrace cleaner returns an empty array for a backtrace if the exception was raised outside the app (inside a gem for instance)
  if backtrace.is_a?(Array) && backtrace.empty?
    exception.backtrace
  else
    backtrace
  end
end

.clear_honeybadger_tags_from_log_contextObject



115
116
117
# File 'lib/exception_handling.rb', line 115

def clear_honeybadger_tags_from_log_context
  @honeybadger_log_context_tags = {}
end

.configured?Boolean

Returns:

  • (Boolean)


44
45
46
# File 'lib/exception_handling.rb', line 44

def configured?
  !@logger.nil?
end

.default_metric_name(exception_data, exception, treat_like_warning) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/exception_handling.rb', line 59

def default_metric_name(exception_data, exception, treat_like_warning)
  if exception_data['metric_name']
    exception_data['metric_name']
  elsif exception.is_a?(ExceptionHandling::Warning)
    "warning"
  elsif treat_like_warning
    exception_name = "_#{exception.class.name.split('::').last}" if exception.present?
    "unforwarded_exception#{exception_name}"
  else
    "exception"
  end
end

.enable_honeybadger(**config) ⇒ Object

Expects passed in hash to only include keys which be directly set on the Honeybadger config



262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/exception_handling.rb', line 262

def enable_honeybadger(**config)
  Bundler.require(:honeybadger)
  Honeybadger.configure do |config_klass|
    config.each do |k, v|
      if k == :before_notify
        config_klass.send(k, v)
      else
        config_klass.send(:"#{k}=", v)
      end
    end
  end
end

.encode_utf8(string) ⇒ Object



327
328
329
330
331
332
# File 'lib/exception_handling.rb', line 327

def encode_utf8(string)
  string.encode('UTF-8',
                replace: '?',
                undef: :replace,
                invalid: :replace)
end

.ensure_completely_safe(exception_context = "", **log_context) ⇒ Object



296
297
298
299
300
301
302
303
# File 'lib/exception_handling.rb', line 296

def ensure_completely_safe(exception_context = "", **log_context)
  yield
rescue SystemExit, SystemStackError, NoMemoryError, SecurityError, SignalException
  raise
rescue Exception => ex
  log_error(ex, exception_context, **log_context)
  nil
end

.ensure_safe(exception_context = "", **log_context) ⇒ Object



289
290
291
292
293
294
# File 'lib/exception_handling.rb', line 289

def ensure_safe(exception_context = "", **log_context)
  yield
rescue => ex
  log_error(ex, exception_context, **log_context)
  nil
end

.exception_catalogObject



92
93
94
# File 'lib/exception_handling.rb', line 92

def exception_catalog
  @exception_catalog ||= ExceptionCatalog.new(@filter_list_filename)
end

.honeybadger_defined?Boolean

Check if Honeybadger defined.

Returns:

  • (Boolean)


255
256
257
# File 'lib/exception_handling.rb', line 255

def honeybadger_defined?
  Object.const_defined?("Honeybadger")
end

.log_debug(message, **log_context) ⇒ Object



285
286
287
# File 'lib/exception_handling.rb', line 285

def log_debug(message, **log_context)
  ExceptionHandling.logger.debug(message, **log_context)
end

.log_error(exception_or_string, exception_context = '', controller = nil, treat_like_warning: false, **log_context, &data_callback) ⇒ Object

Normal Operation:

Called directly by our code, usually from rescue blocks.
Writes to log file and may send to honeybadger

TODO: the **log_context means we can never have context named treat_like_warning. In general, keyword args will be conflated with log_context. Ideally we’d separate to log_context from the other keywords so they don’t interfere in any way. Or have no keyword args.

Functional Test Operation:

Calls into handle_stub_log_error and returns. no log file. no honeybadger


164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/exception_handling.rb', line 164

def log_error(exception_or_string, exception_context = '', controller = nil, treat_like_warning: false, **log_context, &data_callback)
  ex = make_exception(exception_or_string)
  timestamp = set_log_error_timestamp
  exception_info = ExceptionInfo.new(ex, exception_context, timestamp,
                                     controller: controller || current_controller, data_callback: data_callback,
                                     log_context: log_context)

  if stub_handler
    stub_handler.handle_stub_log_error(exception_info.data)
  else
    write_exception_to_log(ex, exception_context, timestamp, log_context)
    external_notification_results = unless treat_like_warning || ex.is_a?(Warning)
                                      send_external_notifications(exception_info)
                                    end || {}
    execute_custom_log_error_callback(exception_info.enhanced_data.merge(log_context: log_context), exception_info.exception, treat_like_warning, external_notification_results)
  end

  ExceptionHandling.last_exception_timestamp
rescue LogErrorStub::UnexpectedExceptionLogged, LogErrorStub::ExpectedExceptionNotLogged
  raise
rescue Exception => ex
  warn("ExceptionHandlingError: log_error rescued exception while logging #{exception_context}: #{exception_or_string}:\n#{ex.class}: #{ex.message}\n#{ex.backtrace.join("\n")}")
  write_exception_to_log(ex, "ExceptionHandlingError: log_error rescued exception while logging #{exception_context}: #{exception_or_string}", timestamp)
ensure
  ExceptionHandling.last_exception_timestamp
end

.log_error_rack(exception, env, _rack_filter) ⇒ Object

Gets called by Rack Middleware: DebugExceptions or ShowExceptions it does 2 things:

log the error
may send to honeybadger

but not during functional tests, when rack middleware is not used



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/exception_handling.rb', line 134

def log_error_rack(exception, env, _rack_filter)
  timestamp = set_log_error_timestamp
  exception_info = ExceptionInfo.new(exception, env, timestamp)

  if stub_handler
    stub_handler.handle_stub_log_error(exception_info.data)
  else
    # TODO: add a more interesting custom description, like:
    # custom_description = ": caught and processed by Rack middleware filter #{rack_filter}"
    # which would be nice, but would also require changing quite a few tests
    custom_description = ""
    write_exception_to_log(exception, custom_description, timestamp)

    send_external_notifications(exception_info)

    nil
  end
end

.log_info(message, **log_context) ⇒ Object



281
282
283
# File 'lib/exception_handling.rb', line 281

def log_info(message, **log_context)
  ExceptionHandling.logger.info(message, **log_context)
end

.log_periodically(exception_key, interval, message, **log_context) ⇒ Object



318
319
320
321
322
323
324
325
# File 'lib/exception_handling.rb', line 318

def log_periodically(exception_key, interval, message, **log_context)
  self.periodic_exception_intervals ||= {}
  last_logged = self.periodic_exception_intervals[exception_key]
  if !last_logged || ((last_logged + interval) < Time.now)
    log_error(message, **log_context)
    self.periodic_exception_intervals[exception_key] = Time.now
  end
end

.log_warning(message, **log_context) ⇒ Object



275
276
277
278
279
# File 'lib/exception_handling.rb', line 275

def log_warning(message, **log_context)
  warning = Warning.new(message)
  warning.set_backtrace([])
  log_error(warning, **log_context)
end

.loggerObject



48
49
50
# File 'lib/exception_handling.rb', line 48

def logger
  @logger or raise ArgumentError, "You must assign a value to #{name}.logger"
end

.logger=(logger) ⇒ Object



52
53
54
55
56
57
# File 'lib/exception_handling.rb', line 52

def logger=(logger)
  logger.nil? || logger.is_a?(ContextualLogger::LoggerMixin) or raise ArgumentError,
                                                                      "The logger must be a ContextualLogger::LoggerMixin, not a #{logger.class}"
  @logger = logger
  EscalateCallback.register_if_configured!
end

.send_exception_to_honeybadger(exception_info) ⇒ Object

Log exception to honeybadger.io.

Returns :success or :failure



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/exception_handling.rb', line 232

def send_exception_to_honeybadger(exception_info)
  exception             = exception_info.exception
  exception_description = exception_info.exception_description

  # Note: Both commas and spaces are treated as delimiters for the :tags string. Space-delimiters are not officially documented.
  # https://github.com/honeybadger-io/honeybadger-ruby/pull/422
  tags = tags_for_honeybadger(exception_info).join(' ')
  response = Honeybadger.notify(error_class: exception_description ? exception_description.filter_name : exception.class.name,
                                error_message: exception.message.to_s,
                                exception:     exception,
                                context:       exception_info.honeybadger_context_data,
                                controller:    exception_info.controller_name,
                                tags:          tags)
  response ? :success : :failure
rescue Exception => ex
  warn("ExceptionHandling.send_exception_to_honeybadger rescued exception while logging #{exception_info.exception_context}:\n#{exception.class}: #{exception.message}:\n#{ex.class}: #{ex.message}\n#{ex.backtrace.join("\n")}")
  write_exception_to_log(ex, "ExceptionHandling.send_exception_to_honeybadger rescued exception while logging #{exception_info.exception_context}:\n#{exception.class}: #{exception.message}", exception_info.timestamp)
  :failure
end

.send_exception_to_honeybadger_unless_filtered(exception_info) ⇒ Object

Returns :success or :failure or :skipped



218
219
220
221
222
223
224
225
# File 'lib/exception_handling.rb', line 218

def send_exception_to_honeybadger_unless_filtered(exception_info)
  if exception_info.send_to_honeybadger?
    send_exception_to_honeybadger(exception_info)
  else
    log_info("Filtered exception using '#{exception_info.exception_description.filter_name}'; not sending notification to Honeybadger")
    :skipped
  end
end

.send_external_notifications(exception_info) ⇒ Object

Send notifications to configured external services



209
210
211
212
213
214
215
# File 'lib/exception_handling.rb', line 209

def send_external_notifications(exception_info)
  results = {}
  if honeybadger_defined?
    results[:honeybadger_status] = send_exception_to_honeybadger_unless_filtered(exception_info)
  end
  results
end

.set_log_error_timestampObject



305
306
307
# File 'lib/exception_handling.rb', line 305

def set_log_error_timestamp
  ExceptionHandling.last_exception_timestamp = Time.now.to_i
end

.trace_timing(description) ⇒ Object



309
310
311
312
313
314
315
316
# File 'lib/exception_handling.rb', line 309

def trace_timing(description)
  result = nil
  time = Benchmark.measure do
    result = yield
  end
  log_info "#{description} %.4fs  " % time.real
  result
end

.write_exception_to_log(ex, exception_context, timestamp, log_context = {}) ⇒ Object

Write an exception out to the log file using our own custom format.



194
195
196
197
198
199
200
201
202
203
204
# File 'lib/exception_handling.rb', line 194

def write_exception_to_log(ex, exception_context, timestamp, log_context = {})
  ActiveSupport::Deprecation.silence do
    log_message = "#{exception_context}\n#{ex.class}: (#{encode_utf8(ex.message.to_s)}):\n  " + clean_backtrace(ex).join("\n  ") + "\n\n"

    if ex.is_a?(Warning)
      ExceptionHandling.logger.warn("\nExceptionHandlingWarning (Warning:#{timestamp}) #{log_message}", **log_context)
    else
      ExceptionHandling.logger.fatal("\nExceptionHandlingError (Error:#{timestamp}) #{log_message}", **log_context)
    end
  end
end