diff --git a/lib/rackstash.rb b/lib/rackstash.rb index e1db00d..b0bdb44 100644 --- a/lib/rackstash.rb +++ b/lib/rackstash.rb @@ -73,6 +73,10 @@ module Rackstash FIELD_TIMESTAMP = '@timestamp'.freeze FIELD_VERSION = '@version'.freeze + FIELD_ERROR = 'error'.freeze + FIELD_ERROR_MESSAGE = 'error_message'.freeze + FIELD_ERROR_TRACE = 'error_trace'.freeze + def self.severity_label(severity) if severity.is_a?(Integer) return SEVERITY_LABELS.last if severity < 0 diff --git a/lib/rackstash/logger.rb b/lib/rackstash/logger.rb index 528c953..73bdb1c 100644 --- a/lib/rackstash/logger.rb +++ b/lib/rackstash/logger.rb @@ -272,6 +272,37 @@ module Rackstash end alias log add + # Extract useful data from an exception and add it to fields of the buffer + # for structured logging. The following fields will be set: + # + # * `error` - The class name of the exception + # * `error_message` - The exception's message + # * `error_trace` - The backtrace of the exception, one frame per line + # + # The exception will not be added to the buffer's `message` field. + # Log it manually with {#add} if desired. + # + # By default, the details of subsequent exceptions will overwrite those of + # older exceptions in the current buffer. Only by the `force` argument to + # `false`, we will preserve existing exceptions. + # + # @param exception [Exception] an Exception object as catched by `rescue` + # @param force [Boolean] set to `false` to preserve the details of an + # existing exception in the current buffer's fields, set to `true` to + # overwrite them. + # @return [Exception] the passed `exception` + def add_exception(exception, force: true) + return exception if !force && buffer.fields[FIELD_ERROR] + + exception_fields = { + FIELD_ERROR => exception.class.name, + FIELD_ERROR_MESSAGE => exception.message, + FIELD_ERROR_TRACE => (exception.backtrace || []).join("\n") + } + buffer.fields.merge!(exception_fields) + exception + end + # Create a new buffering {Buffer} and puts in on the {BufferStack} for the # current Thread. For the duration of the block, all new logged messages # and any access to fields and tags will be sent to this new buffer. diff --git a/spec/rackstash/logger_spec.rb b/spec/rackstash/logger_spec.rb index b80dbfb..927ff18 100644 --- a/spec/rackstash/logger_spec.rb +++ b/spec/rackstash/logger_spec.rb @@ -401,6 +401,72 @@ describe Rackstash::Logger do end end + describe '#add_exception' do + let(:fields) { Rackstash::Fields::Hash.new } + + before(:each) do + buffer = instance_double('Rackstash::Buffer') + allow(buffer).to receive(:fields).and_return(fields) + allow(logger).to receive(:buffer).and_return(buffer) + end + + it 'adds the exception fields' do + begin + raise 'My Error' + rescue => e + logger.add_exception(e) + end + + expect(fields['error']).to eql 'RuntimeError' + expect(fields['error_message']).to eql 'My Error' + expect(fields['error_trace']).to match %r{\A#{__FILE__}:#{__LINE__ - 7}:in} + end + + it 'does not require a backtrace' do + logger.add_exception(StandardError.new('Error')) + + expect(fields['error']).to eql 'StandardError' + expect(fields['error_message']).to eql 'Error' + expect(fields['error_trace']).to eql '' + end + + context 'with force: true' do + it 'overwrites exceptions' do + begin + raise 'Error' + rescue => first + logger.add_exception(first, force: true) + end + + begin + raise TypeError, 'Another Error' + rescue => second + logger.add_exception(second, force: true) + end + + expect(fields['error']).to eql 'TypeError' + expect(fields['error_message']).to eql 'Another Error' + expect(fields['error_trace']).to match %r{\A#{__FILE__}:#{__LINE__ - 7}:in} + end + end + + context 'with force: false' do + it 'does not overwrite exceptions' do + fields['error'] = 'Something is wrong' + + begin + raise TypeError, 'Error' + rescue => second + logger.add_exception(second, force: false) + end + + expect(fields['error']).to eql 'Something is wrong' + expect(fields['error_message']).to be_nil + expect(fields['error_trace']).to be_nil + end + end + end + describe '#with_buffer' do it 'requires a block' do expect { logger.with_buffer }.to raise_error ArgumentError