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.2.1.pre.dc.0'
Class Attribute Summary collapse
-
.current_controller ⇒ Object
internal settings (don't set directly).
-
.custom_data_hook ⇒ Object
Returns the value of attribute custom_data_hook.
-
.environment ⇒ Object
Returns the value of attribute environment.
-
.filter_list_filename ⇒ Object
Returns the value of attribute filter_list_filename.
-
.honeybadger_auto_tagger ⇒ Object
Returns the value of attribute honeybadger_auto_tagger.
-
.honeybadger_log_context_tags ⇒ Object
readonly
Returns the value of attribute honeybadger_log_context_tags.
-
.last_exception_timestamp ⇒ Object
Returns the value of attribute last_exception_timestamp.
-
.periodic_exception_intervals ⇒ Object
Returns the value of attribute periodic_exception_intervals.
-
.post_log_error_hook ⇒ Object
Returns the value of attribute post_log_error_hook.
-
.production_support_recipients ⇒ Object
optional settings.
- .server_name ⇒ Object
-
.stub_handler ⇒ Object
Returns the value of attribute stub_handler.
Class Method Summary collapse
- .add_honeybadger_tag_from_log_context(tag_name, path:) ⇒ Object
- .clean_backtrace(exception) ⇒ Object
- .clear_honeybadger_tags_from_log_context ⇒ Object
- .configured? ⇒ Boolean
- .default_metric_name(exception_data, exception, treat_like_warning) ⇒ Object
-
.enable_honeybadger(**config) ⇒ Object
Expects passed in hash to only include keys which be directly set on the Honeybadger config.
-
.enable_sentry ⇒ Object
Opt in to Sentry notifications.
- .encode_utf8(string) ⇒ Object
- .ensure_completely_safe(exception_context = "", **log_context) ⇒ Object
- .ensure_safe(exception_context = "", **log_context) ⇒ Object
- .exception_catalog ⇒ Object
-
.honeybadger_defined? ⇒ Boolean
Check if Honeybadger defined.
- .log_debug(message, **log_context) ⇒ Object
-
.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.
-
.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.
- .log_info(message, **log_context) ⇒ Object
- .log_periodically(exception_key, interval, message, **log_context) ⇒ Object
- .log_warning(message, **log_context) ⇒ Object
- .logger ⇒ Object
- .logger=(logger) ⇒ Object
-
.send_exception_to_honeybadger(exception_info) ⇒ Object
Log exception to honeybadger.io.
-
.send_exception_to_honeybadger_unless_filtered(exception_info) ⇒ Object
Returns :success or :failure or :skipped.
-
.send_exception_to_sentry(exception_info) ⇒ Object
Log exception to Sentry.
-
.send_exception_to_sentry_unless_filtered(exception_info) ⇒ Object
Returns :success or :failure or :skipped.
-
.send_external_notifications(exception_info) ⇒ Object
Send notifications to configured external services.
-
.sentry_enabled? ⇒ Boolean
Whether ExceptionHandling should notify Sentry.
- .set_log_error_timestamp ⇒ Object
- .trace_timing(description) ⇒ Object
-
.write_exception_to_log(ex, exception_context, timestamp, log_context = {}) ⇒ Object
Write an exception out to the log file using our own custom format.
Class Attribute Details
.current_controller ⇒ Object
internal settings (don't set directly)
122 123 124 |
# File 'lib/exception_handling.rb', line 122 def current_controller @current_controller end |
.custom_data_hook ⇒ Object
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 |
.environment ⇒ Object
Returns the value of attribute environment.
76 77 78 |
# File 'lib/exception_handling.rb', line 76 def environment @environment end |
.filter_list_filename ⇒ Object
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_tagger ⇒ Object
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_tags ⇒ Object (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 end |
.last_exception_timestamp ⇒ Object
Returns the value of attribute last_exception_timestamp.
123 124 125 |
# File 'lib/exception_handling.rb', line 123 def @last_exception_timestamp end |
.periodic_exception_intervals ⇒ Object
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_hook ⇒ Object
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_recipients ⇒ Object
optional settings
75 76 77 |
# File 'lib/exception_handling.rb', line 75 def production_support_recipients @production_support_recipients end |
.server_name ⇒ Object
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_handler ⇒ Object
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
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
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 |
# File 'lib/exception_handling.rb', line 392 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_context ⇒ Object
115 116 117 |
# File 'lib/exception_handling.rb', line 115 def @honeybadger_log_context_tags = {} end |
.configured? ⇒ 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
320 321 322 323 324 325 326 327 328 329 330 331 |
# File 'lib/exception_handling.rb', line 320 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 |
.enable_sentry ⇒ Object
Opt in to Sentry notifications. Requires the Sentry constant and a prior Sentry.init.
311 312 313 314 315 |
# File 'lib/exception_handling.rb', line 311 def enable_sentry Object.const_defined?("Sentry") or raise ArgumentError, "Sentry is not defined" Sentry.initialized? or raise ArgumentError, "Sentry is not initialized" @sentry_enabled = true end |
.encode_utf8(string) ⇒ Object
385 386 387 388 389 390 |
# File 'lib/exception_handling.rb', line 385 def encode_utf8(string) string.encode('UTF-8', replace: '?', undef: :replace, invalid: :replace) end |
.ensure_completely_safe(exception_context = "", **log_context) ⇒ Object
354 355 356 357 358 359 360 361 |
# File 'lib/exception_handling.rb', line 354 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
347 348 349 350 351 352 |
# File 'lib/exception_handling.rb', line 347 def ensure_safe(exception_context = "", **log_context) yield rescue => ex log_error(ex, exception_context, **log_context) nil end |
.exception_catalog ⇒ Object
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.
297 298 299 |
# File 'lib/exception_handling.rb', line 297 def honeybadger_defined? Object.const_defined?("Honeybadger") end |
.log_debug(message, **log_context) ⇒ Object
343 344 345 |
# File 'lib/exception_handling.rb', line 343 def log_debug(, **log_context) ExceptionHandling.logger.debug(, **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) = exception_info = ExceptionInfo.new(ex, exception_context, , 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, , 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. 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.}\n#{ex.backtrace.join("\n")}") write_exception_to_log(ex, "ExceptionHandlingError: log_error rescued exception while logging #{exception_context}: #{exception_or_string}", ) ensure ExceptionHandling. 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) = exception_info = ExceptionInfo.new(exception, env, ) 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, ) send_external_notifications(exception_info) nil end end |
.log_info(message, **log_context) ⇒ Object
339 340 341 |
# File 'lib/exception_handling.rb', line 339 def log_info(, **log_context) ExceptionHandling.logger.info(, **log_context) end |
.log_periodically(exception_key, interval, message, **log_context) ⇒ Object
376 377 378 379 380 381 382 383 |
# File 'lib/exception_handling.rb', line 376 def log_periodically(exception_key, interval, , **log_context) self.periodic_exception_intervals ||= {} last_logged = self.periodic_exception_intervals[exception_key] if !last_logged || ((last_logged + interval) < Time.now) log_error(, **log_context) self.periodic_exception_intervals[exception_key] = Time.now end end |
.log_warning(message, **log_context) ⇒ Object
333 334 335 336 337 |
# File 'lib/exception_handling.rb', line 333 def log_warning(, **log_context) warning = Warning.new() warning.set_backtrace([]) log_error(warning, **log_context) end |
.logger ⇒ Object
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
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/exception_handling.rb', line 235 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 = (exception_info).join(' ') response = Honeybadger.notify(error_class: exception_description ? exception_description.filter_name : exception.class.name, error_message: exception..to_s, exception: exception, context: exception_info.honeybadger_context_data, controller: exception_info.controller_name, 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.}:\n#{ex.class}: #{ex.}\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.}", exception_info.) :failure end |
.send_exception_to_honeybadger_unless_filtered(exception_info) ⇒ Object
Returns :success or :failure or :skipped
221 222 223 224 225 226 227 228 |
# File 'lib/exception_handling.rb', line 221 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_exception_to_sentry(exception_info) ⇒ Object
Log exception to Sentry.
Returns :success or :failure
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/exception_handling.rb', line 270 def send_exception_to_sentry(exception_info) exception = exception_info.exception exception_description = exception_info.exception_description = (exception_info) exception_handling_context = exception_info.honeybadger_context_data controller_context = exception_info.controller_name.present? ? { name: exception_info.controller_name } : nil fingerprint_data = exception_description ? [exception_description.filter_name.to_s] : nil response = Sentry.capture_exception(exception) do |scope| log_info("!!! Setting Sentry scope") scope.() if .any? scope.set_context("exception_handling", exception_handling_context) scope.set_context("controller", controller_context) if controller_context scope.set_fingerprint(fingerprint_data) if fingerprint_data log_info("!!! Finished setting Sentry scope") end response ? :success : :failure rescue Exception => ex warn("ExceptionHandling.send_exception_to_sentry rescued exception while logging #{exception_info.exception_context}:\n#{exception.class}: #{exception.}:\n#{ex.class}: #{ex.}\n#{ex.backtrace.join("\n")}") write_exception_to_log(ex, "ExceptionHandling.send_exception_to_sentry rescued exception while logging #{exception_info.exception_context}:\n#{exception.class}: #{exception.}", exception_info.) :failure end |
.send_exception_to_sentry_unless_filtered(exception_info) ⇒ Object
Returns :success or :failure or :skipped
256 257 258 259 260 261 262 263 |
# File 'lib/exception_handling.rb', line 256 def send_exception_to_sentry_unless_filtered(exception_info) if exception_info.send_to_sentry? send_exception_to_sentry(exception_info) else log_info("Filtered exception using '#{exception_info.exception_description.filter_name}'; not sending notification to Sentry") :skipped end end |
.send_external_notifications(exception_info) ⇒ Object
Send notifications to configured external services
209 210 211 212 213 214 215 216 217 218 |
# 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 if sentry_enabled? results[:sentry_status] = send_exception_to_sentry_unless_filtered(exception_info) end results end |
.sentry_enabled? ⇒ Boolean
Whether ExceptionHandling should notify Sentry. Off by default; call enable_sentry after Sentry.init.
304 305 306 |
# File 'lib/exception_handling.rb', line 304 def sentry_enabled? !!@sentry_enabled end |
.set_log_error_timestamp ⇒ Object
363 364 365 |
# File 'lib/exception_handling.rb', line 363 def ExceptionHandling. = Time.now.to_i end |
.trace_timing(description) ⇒ Object
367 368 369 370 371 372 373 374 |
# File 'lib/exception_handling.rb', line 367 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, , log_context = {}) with_deprecations_silenced do = "#{exception_context}\n#{ex.class}: (#{encode_utf8(ex..to_s)}):\n " + clean_backtrace(ex).join("\n ") + "\n\n" if ex.is_a?(Warning) ExceptionHandling.logger.warn("\nExceptionHandlingWarning (Warning:#{}) #{}", **log_context) else ExceptionHandling.logger.fatal("\nExceptionHandlingError (Error:#{}) #{}", **log_context) end end end |