From bb734890c1a7f736c160bdd6a8a14b218d654c5a Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 29 Nov 2024 14:36:01 +0000 Subject: [PATCH] Add `error_cause` property to log message. (#39) --- .rubocop.yml | 3 +++ lib/sidekiq/exception_utils.rb | 24 +++++++++++++++--------- lib/sidekiq/logging/shared.rb | 13 +++++++++++-- spec/sidekiq/logstash_spec.rb | 16 ++++++++++++++-- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 50ac415..a42b1a1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -13,6 +13,9 @@ Metrics/BlockLength: Exclude: - spec/**/*.rb +Metrics/ModuleLength: + Enabled: false + Layout/LineLength: Max: 120 Exclude: diff --git a/lib/sidekiq/exception_utils.rb b/lib/sidekiq/exception_utils.rb index f7e1765..b56df47 100644 --- a/lib/sidekiq/exception_utils.rb +++ b/lib/sidekiq/exception_utils.rb @@ -2,25 +2,31 @@ # Utility that allows us to get a hash representation of an exception module ExceptionUtils - def self.get_exception_with_cause_hash(exc, parent_backtrace = nil, max_depth_left) - backtrace = exc.backtrace || [] - if parent_backtrace - common_lines = backtrace.reverse.zip(parent_backtrace.reverse).take_while { |a, b| a == b } - - backtrace = backtrace[0...-common_lines.length] if common_lines.any? - end + module_function + def get_exception_with_cause_hash(exc, parent_backtrace = nil, max_depth_left: 1) error_hash = { 'class' => exc.class.to_s, 'message' => exc.message, - 'backtrace' => backtrace + 'backtrace' => backtrace_for(exc, parent_backtrace) } if (cause = exc.cause) && max_depth_left.positive? # Pass the current backtrace as the parent_backtrace to the cause to shorten cause's backtrace list - error_hash['cause'] = get_exception_with_cause_hash(cause, exc.backtrace, max_depth_left - 1) + error_hash['cause'] = get_exception_with_cause_hash(cause, exc.backtrace, max_depth_left: max_depth_left - 1) end error_hash end + + def backtrace_for(exception, parent_backtrace = nil) + backtrace = exception.backtrace || [] + if parent_backtrace + common_lines = backtrace.reverse.zip(parent_backtrace.reverse).take_while { |a, b| a == b } + + backtrace = backtrace[0...-common_lines.length] if common_lines.any? + end + + backtrace + end end diff --git a/lib/sidekiq/logging/shared.rb b/lib/sidekiq/logging/shared.rb index 4001fd9..3be3f8f 100644 --- a/lib/sidekiq/logging/shared.rb +++ b/lib/sidekiq/logging/shared.rb @@ -54,12 +54,21 @@ def log_job_exception(job, started_at, exc) config = Sidekiq::Logstash.configuration if config.log_job_exception_with_causes - payload['error'] = ExceptionUtils.get_exception_with_cause_hash(exc, config.causes_logging_max_depth) + payload['error'] = ExceptionUtils.get_exception_with_cause_hash( + exc, max_depth_left: config.causes_logging_max_depth + ) else exc = exc.cause || exc if exc.is_a? Sidekiq::JobRetry::Handled payload['error_message'] = exc.message - payload['error'] = exc.class + payload['error'] = exc.class.to_s payload['error_backtrace'] = %('#{exc.backtrace.join("\n")}') + if (cause = exc.cause) + payload['error_cause'] = { + 'class' => cause.class.to_s, + 'message' => cause.message, + 'backtrace' => ExceptionUtils.backtrace_for(cause, exc.backtrace) + } + end end process_payload(payload) diff --git a/spec/sidekiq/logstash_spec.rb b/spec/sidekiq/logstash_spec.rb index c6bfd1b..efb91e5 100644 --- a/spec/sidekiq/logstash_spec.rb +++ b/spec/sidekiq/logstash_spec.rb @@ -120,9 +120,15 @@ def process(worker, params = [], encrypt: false) it 'logs the exception with job retry' do expect { process(SpecWorker, [true]) }.to raise_error(RuntimeError) - expect(log_message['error_message']).to eq('You know nothing, Jon Snow.') expect(log_message['error']).to eq('RuntimeError') + expect(log_message['error_message']).to eq('You know nothing, Jon Snow.') expect(log_message['error_backtrace'].split("\n").first).to include('workers/spec_worker.rb:') + expect(log_message['error_cause']).to match( + hash_including( + 'class' => 'RuntimeError', + 'message' => 'Error rescuing error' + ) + ) end it 'logs the exception without job retry' do @@ -130,9 +136,15 @@ def process(worker, params = [], encrypt: false) expect { process(SpecWorker, [true]) }.to raise_error(RuntimeError) - expect(log_message['error_message']).to eq('You know nothing, Jon Snow.') expect(log_message['error']).to eq('RuntimeError') + expect(log_message['error_message']).to eq('You know nothing, Jon Snow.') expect(log_message['error_backtrace'].split("\n").first).to include('workers/spec_worker.rb:') + expect(log_message['error_cause']).to match( + hash_including( + 'class' => 'RuntimeError', + 'message' => 'Error rescuing error' + ) + ) end context 'log_job_exception_with_causes enabled' do