diff --git a/Gemfile.lock b/Gemfile.lock index 5ea9171..74f5fb2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - contextual_logger (0.2.1) + contextual_logger (0.3.0) json GEM diff --git a/contextual_logger.gemspec b/contextual_logger.gemspec index 35d2818..fe24e2c 100644 --- a/contextual_logger.gemspec +++ b/contextual_logger.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |spec| spec.name = 'contextual_logger' - spec.version = '0.2.1' + spec.version = '0.3.0' spec.license = 'MIT' spec.date = '2018-10-12' spec.summary = 'Add context to your logger' diff --git a/lib/contextual_logger.rb b/lib/contextual_logger.rb index a53c312..5cd3dc2 100644 --- a/lib/contextual_logger.rb +++ b/lib/contextual_logger.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'json' +require_relative './contextual_logger/context/handler' module ContextualLogger def self.new(logger) @@ -8,19 +9,23 @@ def self.new(logger) end def global_context=(context) - Thread.current[THREAD_CONTEXT_NAMESPACE] = context + ContextualLogger::Context::Handler.new(context).set! end def with_context(context) - previous_context = Thread.current[THREAD_CONTEXT_NAMESPACE] || {} - Thread.current[THREAD_CONTEXT_NAMESPACE] = previous_context.merge(context) - yield if block_given? + context_handler = ContextualLogger::Context::Handler.new(current_context_for_thread.merge(context)) + context_handler.set! + if block_given? + yield + else + context_handler + end ensure - Thread.current[THREAD_CONTEXT_NAMESPACE] = previous_context + context_handler.reset! if block_given? end def current_context_for_thread - Thread.current[THREAD_CONTEXT_NAMESPACE] || {} + ContextualLogger::Context::Handler.current_context end def format_message(severity, timestamp, progname, message, context) @@ -81,8 +86,6 @@ def write_entry_to_log(severity, timestamp, progname, message, context) private - THREAD_CONTEXT_NAMESPACE = 'ContextualLoggerCurrentLoggingContext' - def message_with_context(context, message, severity, timestamp, progname) context.merge( message: message, diff --git a/lib/contextual_logger/context/handler.rb b/lib/contextual_logger/context/handler.rb new file mode 100644 index 0000000..b0066d6 --- /dev/null +++ b/lib/contextual_logger/context/handler.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module ContextualLogger + module Context + class Handler + THREAD_CONTEXT_NAMESPACE = 'ContextualLoggerCurrentLoggingContext' + + attr_reader :previous_context, :context + + def self.current_context + Thread.current[THREAD_CONTEXT_NAMESPACE] || {} + end + + def initialize(context, previous_context: nil) + @previous_context = previous_context || self.class.current_context + @context = context + end + + def set! + Thread.current[THREAD_CONTEXT_NAMESPACE] = context + end + + def reset! + Thread.current[THREAD_CONTEXT_NAMESPACE] = previous_context + end + end + end +end diff --git a/spec/lib/context/handler_spec.rb b/spec/lib/context/handler_spec.rb new file mode 100644 index 0000000..b5c701f --- /dev/null +++ b/spec/lib/context/handler_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'contextual_logger' + +describe ContextualLogger::Context::Handler do + let(:context) { { service: "hello_world", integration: "google" } } + subject(:handler) { ContextualLogger::Context::Handler.new(context) } + + it { is_expected.to respond_to(:set!) } + it { is_expected.to respond_to(:reset!) } + + it 'sets the thread context on set!' do + previous_context = ContextualLogger::Context::Handler.current_context + handler.set! + + expect(ContextualLogger::Context::Handler.current_context).to_not eq(previous_context) + expect(ContextualLogger::Context::Handler.current_context).to eq(context) + + handler.reset! + end + + it 'resets the thread context on reset!' do + initial_context = ContextualLogger::Context::Handler.current_context + handler.set! + new_context = ContextualLogger::Context::Handler.current_context + handler.reset! + + expect(ContextualLogger::Context::Handler.current_context).to_not eq(new_context) + expect(ContextualLogger::Context::Handler.current_context).to eq(initial_context) + end +end diff --git a/spec/lib/contextual_logger_spec.rb b/spec/lib/contextual_logger_spec.rb index 618c965..4fe83f4 100644 --- a/spec/lib/contextual_logger_spec.rb +++ b/spec/lib/contextual_logger_spec.rb @@ -8,6 +8,7 @@ before do Time.now_override = Time.now @logger = ContextualLogger.new(Logger.new('/dev/null')) + @logger.global_context = {} end it 'should respond to with_context' do @@ -169,6 +170,83 @@ end end end + + it 'returns the output of the block passed in' do + expect(@logger.with_context(service: 'test_service') { 6 }).to eq(6) + end + end + + describe 'with_context without block' do + it 'returns a context handler' do + expect(@logger.with_context(service: 'test_service')).to be_a(ContextualLogger::Context::Handler) + end + + it 'prints out the wrapper context with logging' do + expected_log_line = { + service: 'test_service', + message: 'this is a test', + severity: 'INFO', + timestamp: Time.now, + progname: nil + }.to_json + + expect_any_instance_of(Logger::LogDevice).to receive(:write).with("#{expected_log_line}\n") + + handler = @logger.with_context(service: 'test_service') + expect(@logger.info('this is a test')).to eq(true) + handler.reset! + end + + it 'merges inline context into wrapper context when logging' do + expected_log_line = { + service: 'test_service', + file: 'this_file.json', + message: 'this is a test', + severity: 'INFO', + timestamp: Time.now, + progname: nil + }.to_json + + expect_any_instance_of(Logger::LogDevice).to receive(:write).with("#{expected_log_line}\n") + + handler = @logger.with_context(service: 'test_service') + expect(@logger.info('this is a test', file: 'this_file.json')).to eq(true) + handler.reset! + end + + it 'takes inline context over wrapper context when logging' do + expected_log_line = { + service: 'test_service_2', + message: 'this is a test', + severity: 'INFO', + timestamp: Time.now, + progname: nil + }.to_json + + expect_any_instance_of(Logger::LogDevice).to receive(:write).with("#{expected_log_line}\n") + + handler = @logger.with_context(service: 'test_service') + expect(@logger.info('this is a test', service: 'test_service_2')).to eq(true) + handler.reset! + end + + it 'combines tiered contexts when logging' do + expected_log_line = { + service: 'test_service', + file: 'this_file.json', + message: 'this is a test', + severity: 'INFO', + timestamp: Time.now, + progname: nil + }.to_json + + expect_any_instance_of(Logger::LogDevice).to receive(:write).with("#{expected_log_line}\n") + + handler1 = @logger.with_context(service: 'test_service') + @logger.with_context(file: 'this_file.json') + expect(@logger.info('this is a test')).to eq(true) + handler1.reset! + end end describe 'global_context' do