diff --git a/Guardfile b/Guardfile index 1542d79..e4f2f3e 100644 --- a/Guardfile +++ b/Guardfile @@ -9,6 +9,8 @@ group :sinatra do all_on_start: true, cmd: "RACK_ENV=#{env} bundle exec appraisal sinatra14 rspec" do watch(%r{^spec/integration/sinatra_spec.rb$}) + watch('lib/loga/context_manager.rb') + watch('spec/loga/context_manager_spec.rb') end end end @@ -34,6 +36,8 @@ group :rails do end watch(%r{^spec/integration/rails/.+_spec\.rb$}) + watch('lib/loga/context_manager.rb') + watch('spec/loga/context_manager_spec.rb') end end end @@ -50,6 +54,8 @@ group :sidekiq do 'spec/loga/sidekiq_spec.rb', ] end + watch('lib/loga/context_manager.rb') + watch('spec/loga/context_manager_spec.rb') end end @@ -57,5 +63,7 @@ group :unit do guard :rspec, cmd: 'bundle exec appraisal unit rspec' do watch(%r{^spec/unit/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" } + watch('lib/loga/context_manager.rb') + watch('spec/loga/context_manager_spec.rb') end end diff --git a/lib/loga.rb b/lib/loga.rb index 20242c8..04285a6 100644 --- a/lib/loga.rb +++ b/lib/loga.rb @@ -9,32 +9,53 @@ require 'loga/rack/request_id' require 'loga/railtie' if defined?(Rails) require 'loga/sidekiq' +require 'loga/context_manager' module Loga ConfigurationError = Class.new(StandardError) - def self.configuration - unless @configuration - raise ConfigurationError, - 'Loga has not been configured. Configure with Loga.configure(options)' + class << self + def configuration + unless @configuration + raise ConfigurationError, + 'Loga has not been configured. Configure with Loga.configure(options)' + end + + @configuration end - @configuration - end + def configure(options, framework_options = {}) + raise ConfigurationError, 'Loga has already been configured' if @configuration - def self.configure(options, framework_options = {}) - raise ConfigurationError, 'Loga has already been configured' if @configuration + @configuration ||= Configuration.new(options, framework_options) - @configuration ||= Configuration.new(options, framework_options) + Loga::Sidekiq.configure_logging + end - Loga::Sidekiq.configure_logging - end + def logger + configuration.logger + end - def self.logger - configuration.logger - end + def reset + @configuration = nil + end - def self.reset - @configuration = nil + def attach_context(payload) + current_context.attach_context(payload) + end + + def clear_context + current_context.clear + end + + def retrieve_context + current_context.retrieve_context + end + + private + + def current_context + ContextManager.current + end end end diff --git a/lib/loga/configuration.rb b/lib/loga/configuration.rb index f0e22ec..43d0867 100644 --- a/lib/loga/configuration.rb +++ b/lib/loga/configuration.rb @@ -14,44 +14,42 @@ class Configuration Sinatra::NotFound ].freeze - attr_accessor :device, :filter_exceptions, :filter_parameters, - :host, :level, :service_version, :sync, :tags, :hide_pii - attr_reader :logger, :format, :service_name + attr_reader :device, :filter_exceptions, :filter_parameters, :format, :hide_pii, + :host, :level, :service_name, :service_version, :sync, :tags - # rubocop:disable Metrics/MethodLength def initialize(user_options = {}, framework_options = {}) options = default_options.merge(framework_options) .merge(environment_options) .merge(user_options) - self.device = options[:device] - self.filter_exceptions = options[:filter_exceptions] - self.filter_parameters = options[:filter_parameters] - self.format = options[:format] - self.host = options[:host] - self.level = options[:level] - self.service_name = options[:service_name] - self.service_version = options[:service_version] || ServiceVersionStrategies.call - self.sync = options[:sync] - self.tags = options[:tags] - self.hide_pii = options[:hide_pii] + @device = options[:device] + @filter_exceptions = options[:filter_exceptions] + @filter_parameters = options[:filter_parameters] + @format = options[:format].to_s.to_sym + @hide_pii = options[:hide_pii] + @host = options[:host] + @level = options[:level] + @service_name = options[:service_name].to_s.strip + @service_version = options[:service_version] || ServiceVersionStrategies.call + @sync = options[:sync] + @tags = options[:tags] validate - - @logger = initialize_logger end - # rubocop:enable Metrics/MethodLength - def format=(name) - @format = name.to_s.to_sym + def structured? + format == :gelf end - def service_name=(name) - @service_name = name.to_s.strip - end + def logger + @logger ||= begin + device.sync = sync + new_logger = Logger.new(device) + new_logger.formatter = assign_formatter + new_logger.level = constantized_log_level - def structured? - format == :gelf + TaggedLogging.new(new_logger) + end end private @@ -79,14 +77,6 @@ def environment_options { format: ENV['LOGA_FORMAT'].presence }.reject { |_, v| v.nil? } end - def initialize_logger - device.sync = sync - logger = Logger.new(device) - logger.formatter = assign_formatter - logger.level = constantized_log_level - TaggedLogging.new(logger) - end - def constantized_log_level Logger.const_get(level.to_s.upcase) end @@ -98,15 +88,13 @@ def hostname end def assign_formatter - if format == :gelf - Formatters::GELFFormatter.new( - service_name: service_name, - service_version: service_version, - host: host, - ) - else - Formatters::SimpleFormatter.new - end + return Formatters::SimpleFormatter.new if format != :gelf + + Formatters::GELFFormatter.new( + service_name: service_name, + service_version: service_version, + host: host, + ) end end end diff --git a/lib/loga/context_manager.rb b/lib/loga/context_manager.rb new file mode 100644 index 0000000..1204455 --- /dev/null +++ b/lib/loga/context_manager.rb @@ -0,0 +1,31 @@ +require 'thread' + +module Loga + class ContextManager + def self.current + Thread.current[:__loga_context_manager] ||= new + end + + def initialize + @semaphore = Mutex.new + + clear + end + + def clear + @semaphore.synchronize do + @context = {} + end + end + + def attach_context(payload) + @semaphore.synchronize do + @context.update(payload) + end + end + + def retrieve_context + @semaphore.synchronize { @context } + end + end +end diff --git a/lib/loga/formatters/gelf_formatter.rb b/lib/loga/formatters/gelf_formatter.rb index bbc9b40..bbaa13b 100644 --- a/lib/loga/formatters/gelf_formatter.rb +++ b/lib/loga/formatters/gelf_formatter.rb @@ -48,12 +48,15 @@ def build_event(time, message) event.timestamp ||= time event.data ||= {} - event.data.tap do |hash| - hash.merge! compute_exception(event.exception) - hash.merge! compute_type(event.type) + + event.data.tap do |data_hash| + data_hash.merge!(compute_exception(event.exception)) + data_hash.merge!(compute_type(event.type)) # Overwrite hash with Loga's additional fields - hash.merge! loga_additional_fields + data_hash.merge!(loga_additional_fields) + data_hash.merge!(Loga.retrieve_context) end + event end diff --git a/lib/loga/rack/logger.rb b/lib/loga/rack/logger.rb index 81e880b..f6cffad 100644 --- a/lib/loga/rack/logger.rb +++ b/lib/loga/rack/logger.rb @@ -43,7 +43,7 @@ def set_data data['request_ip'] = request.ip data['user_agent'] = request.user_agent data['controller'] = request.controller_action_name if request.controller_action_name - data['duration'] = duration_in_ms(started_at, Time.now) + data['duration'] = duration_in_ms(started_at) end # rubocop:enable Metrics/LineLength @@ -55,6 +55,7 @@ def send_message timestamp: started_at, type: 'request', ) + logger.public_send(compute_level, event) end diff --git a/spec/fixtures/rails32.rb b/spec/fixtures/rails32.rb index f4ba24f..eb029c7 100644 --- a/spec/fixtures/rails32.rb +++ b/spec/fixtures/rails32.rb @@ -49,6 +49,12 @@ def update @id = params[:id] render '/user' end + + def attach_context + Loga.attach_context(animal: 'monkey') + + render json: { hello: :world } + end end class FakeMailer < ActionMailer::Base @@ -68,6 +74,27 @@ def basic_mail end end +class FakeMailerWithContext < ActionMailer::Base + default from: 'notifications@example.com' + + def self.send_email + Loga.attach_context(tshirt_size: 'XL') + Loga.attach_context(height: 155) + Loga.attach_context(weight: 122) + + basic_mail.deliver + end + + def basic_mail + mail( + to: 'user@example.com', + subject: 'Welcome to My Awesome Site', + body: 'Banana muffin', + content_type: 'text/html', + ) + end +end + Dummy.routes.append do get 'ok' => 'application#ok' get 'error' => 'application#error' @@ -75,6 +102,8 @@ def basic_mail post 'users' => 'application#create' get 'new' => 'application#new' put 'users/:id' => 'application#update' + + get 'attach_context' => 'application#attach_context' end Dummy.initialize! diff --git a/spec/fixtures/rails40.rb b/spec/fixtures/rails40.rb index b33aa20..7b060b1 100644 --- a/spec/fixtures/rails40.rb +++ b/spec/fixtures/rails40.rb @@ -49,6 +49,12 @@ def update @id = params[:id] render '/user' end + + def attach_context + Loga.attach_context(animal: 'monkey') + + render json: { hello: :world } + end end class FakeMailer < ActionMailer::Base @@ -68,6 +74,27 @@ def basic_mail end end +class FakeMailerWithContext < ActionMailer::Base + default from: 'notifications@example.com' + + def self.send_email + Loga.attach_context(tshirt_size: 'XL') + Loga.attach_context(height: 155) + Loga.attach_context(weight: 122) + + basic_mail.deliver + end + + def basic_mail + mail( + to: 'user@example.com', + subject: 'Welcome to My Awesome Site', + body: 'Banana muffin', + content_type: 'text/html', + ) + end +end + Dummy.routes.append do get 'ok' => 'application#ok' get 'error' => 'application#error' @@ -75,6 +102,8 @@ def basic_mail post 'users' => 'application#create' get 'new' => 'application#new' put 'users/:id' => 'application#update' + + get 'attach_context' => 'application#attach_context' end Dummy.initialize! diff --git a/spec/fixtures/rails42.rb b/spec/fixtures/rails42.rb index 6e7995c..c02c9eb 100644 --- a/spec/fixtures/rails42.rb +++ b/spec/fixtures/rails42.rb @@ -49,6 +49,12 @@ def update @id = params[:id] render '/user' end + + def attach_context + Loga.attach_context(animal: 'monkey') + + render json: { hello: :world } + end end class FakeMailer < ActionMailer::Base @@ -68,6 +74,27 @@ def basic_mail end end +class FakeMailerWithContext < ActionMailer::Base + default from: 'notifications@example.com' + + def self.send_email + Loga.attach_context(tshirt_size: 'XL') + Loga.attach_context(height: 155) + Loga.attach_context(weight: 122) + + basic_mail.deliver_now + end + + def basic_mail + mail( + to: 'user@example.com', + subject: 'Welcome to My Awesome Site', + body: 'Banana muffin', + content_type: 'text/html', + ) + end +end + Dummy.routes.append do get 'ok' => 'application#ok' get 'error' => 'application#error' @@ -75,6 +102,8 @@ def basic_mail post 'users' => 'application#create' get 'new' => 'application#new' put 'users/:id' => 'application#update' + + get 'attach_context' => 'application#attach_context' end Dummy.initialize! diff --git a/spec/fixtures/rails50.rb b/spec/fixtures/rails50.rb index 1fae3cb..da03f27 100644 --- a/spec/fixtures/rails50.rb +++ b/spec/fixtures/rails50.rb @@ -49,6 +49,12 @@ def update @id = params[:id] render '/user' end + + def attach_context + Loga.attach_context(animal: 'monkey') + + render json: { hello: :world } + end end class FakeMailer < ActionMailer::Base @@ -68,6 +74,27 @@ def basic_mail end end +class FakeMailerWithContext < ActionMailer::Base + default from: 'notifications@example.com' + + def self.send_email + Loga.attach_context(tshirt_size: 'XL') + Loga.attach_context(height: 155) + Loga.attach_context(weight: 122) + + basic_mail.deliver_now + end + + def basic_mail + mail( + to: 'user@example.com', + subject: 'Welcome to My Awesome Site', + body: 'Banana muffin', + content_type: 'text/html', + ) + end +end + Dummy.routes.append do get 'ok' => 'application#ok' get 'error' => 'application#error' @@ -75,6 +102,8 @@ def basic_mail post 'users' => 'application#create' get 'new' => 'application#new' put 'users/:id' => 'application#update' + + get 'attach_context' => 'application#attach_context' end Dummy.initialize! diff --git a/spec/fixtures/rails52.rb b/spec/fixtures/rails52.rb index 9c5ddf1..ea244a3 100644 --- a/spec/fixtures/rails52.rb +++ b/spec/fixtures/rails52.rb @@ -49,6 +49,12 @@ def update @id = params[:id] render '/user' end + + def attach_context + Loga.attach_context(animal: 'monkey') + + render json: { hello: :world } + end end class FakeMailer < ActionMailer::Base @@ -68,6 +74,27 @@ def basic_mail end end +class FakeMailerWithContext < ActionMailer::Base + default from: 'notifications@example.com' + + def self.send_email + Loga.attach_context(tshirt_size: 'XL') + Loga.attach_context(height: 155) + Loga.attach_context(weight: 122) + + basic_mail.deliver_now + end + + def basic_mail + mail( + to: 'user@example.com', + subject: 'Welcome to My Awesome Site', + body: 'Banana muffin', + content_type: 'text/html', + ) + end +end + Dummy.routes.append do get 'ok' => 'application#ok' get 'error' => 'application#error' @@ -75,6 +102,8 @@ def basic_mail post 'users' => 'application#create' get 'new' => 'application#new' put 'users/:id' => 'application#update' + + get 'attach_context' => 'application#attach_context' end Dummy.initialize! diff --git a/spec/integration/rails/action_mailer_spec.rb b/spec/integration/rails/action_mailer_spec.rb index 47f1ceb..4ca419c 100644 --- a/spec/integration/rails/action_mailer_spec.rb +++ b/spec/integration/rails/action_mailer_spec.rb @@ -59,5 +59,17 @@ expect(last_log_entry).to have_key('_unique_id') end + + it 'attaches custom context' do + FakeMailerWithContext.send_email + + expected_context = { + '_tshirt_size' => 'XL', + '_height' => 155, + '_weight' => 122, + } + + expect(last_log_entry).to include(expected_context) + end end end diff --git a/spec/integration/rails/request_spec.rb b/spec/integration/rails/request_spec.rb index 9e6f338..7c1c727 100644 --- a/spec/integration/rails/request_spec.rb +++ b/spec/integration/rails/request_spec.rb @@ -6,12 +6,14 @@ let(:log_entries) do entries = [] + STREAM.tap do |s| s.rewind entries = s.read.split("\n").map { |line| JSON.parse(line) } s.close s.reopen end + entries end @@ -22,43 +24,56 @@ it 'preserves rails parameters' do get '/show' + expect(json_response).to eq('action' => 'show', 'controller' => 'application') end it 'includes the controller name and action' do get '/ok' + expect(last_log_entry).to include('_request.controller' => 'ApplicationController#ok') end - describe 'LogSubscriber' do - describe 'ActionController' do - let(:action_controller_notifications) do - log_entries.select { |e| e.to_json =~ /Processing by|Completed/ } - end + describe 'ActionController::LogSubscriber' do + let(:action_controller_notifications) do + log_entries.select { |e| e.to_json =~ /Processing by|Completed/ } + end - it 'silences ActionController::LogSubscriber' do - get '/show' - expect(action_controller_notifications).to be_empty - end + it 'silences ActionController::LogSubscriber' do + get '/show' + + expect(action_controller_notifications).to be_empty end + end - describe 'ActionView' do - let(:action_view_notifications) do - log_entries.select { |e| e.to_json =~ /Rendered/ } - end + describe 'ActionView::LogSubscriber' do + let(:action_view_notifications) do + log_entries.select { |e| e.to_json =~ /Rendered/ } + end - it 'silences ActionView::LogSubscriber' do - put '/users/5' - expect(action_view_notifications).to be_empty - end + it 'silences ActionView::LogSubscriber' do + put '/users/5' + + expect(action_view_notifications).to be_empty end end - describe 'when request causes ActionDispatch 404' do + context 'when request causes ActionDispatch 404' do it 'does not log ActionDispatch::DebugExceptions' do get '/not_found', {}, 'HTTP_X_REQUEST_ID' => '471a34dc' - expect(log_entries.count).to eq(1) - expect(last_log_entry['short_message']).to eq('GET /not_found 404 in 0ms') + + aggregate_failures do + expect(log_entries.count).to eq(1) + expect(last_log_entry['short_message']).to eq('GET /not_found 404 in 0ms') + end + end + end + + context 'when custom context is attached' do + it 'adds context to the log' do + get '/attach_context' + + expect(last_log_entry['_animal']).to eq 'monkey' end end end diff --git a/spec/integration/sidekiq_spec.rb b/spec/integration/sidekiq_spec.rb index 62f83b5..aebf33b 100644 --- a/spec/integration/sidekiq_spec.rb +++ b/spec/integration/sidekiq_spec.rb @@ -12,10 +12,20 @@ config.redis = dummy_redis_config end -class MySidekiqWorker - include Sidekiq::Worker +module Loga + class MySidekiqWorker + include ::Sidekiq::Worker - def perform(_name); end + def perform(_name); end + end + + class DummyWorkerWithContext + include ::Sidekiq::Worker + + def perform(name) + Loga.attach_context(name_upcased: name.upcase) + end + end end describe 'Sidekiq client logger' do @@ -54,12 +64,12 @@ def perform(_name); end end it 'pushes a new element in the default queue' do - MySidekiqWorker.perform_async('Bob') + Loga::MySidekiqWorker.perform_async('Bob') last_element = JSON.parse(Redis.current.lpop('queue:default')) aggregate_failures do - expect(last_element['class']).to eq 'MySidekiqWorker' + expect(last_element['class']).to eq 'Loga::MySidekiqWorker' expect(last_element['args']).to eq ['Bob'] expect(last_element['retry']).to eq true expect(last_element['queue']).to eq 'default' @@ -98,7 +108,7 @@ def options end it 'logs the job processing event' do - MySidekiqWorker.perform_async('Bob') + Loga::MySidekiqWorker.perform_async('Bob') require 'sidekiq/processor' Sidekiq::Processor.new(mgr.new).start @@ -108,7 +118,7 @@ def options '_queue'=> 'default', '_retry'=> true, '_params'=> ['Bob'], - '_class'=> 'MySidekiqWorker', + '_class'=> 'Loga::MySidekiqWorker', '_type'=> 'sidekiq', '_service.name'=> 'hello_world_app', '_service.version'=> '1.0', @@ -124,8 +134,18 @@ def options expect(json_line).to have_key(key) end - expect(json_line['short_message']).to match(/MySidekiqWorker with jid:*/) + expect(json_line['short_message']).to match(/Loga::MySidekiqWorker with jid:*/) end end + + it 'attaches a custom context' do + Loga::DummyWorkerWithContext.perform_async('John Doe') + + require 'sidekiq/processor' + Sidekiq::Processor.new(mgr.new).start + sleep 0.5 + + expect(json_line['_name_upcased']).to eq 'JOHN DOE' + end end end diff --git a/spec/integration/sinatra_spec.rb b/spec/integration/sinatra_spec.rb index d304565..44ea015 100644 --- a/spec/integration/sinatra_spec.rb +++ b/spec/integration/sinatra_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -class MySinatraApp < Sinatra::Base +Loga::MySinatraApp = Class.new(Sinatra::Base) do set :logging, false get '/ok' do @@ -19,27 +19,35 @@ class MySinatraApp < Sinatra::Base get '/new' do redirect '/ok' end + + get '/attach_context' do + Loga.attach_context(fruit: 'banana') + + 'I am a banana' + end end RSpec.describe 'Structured logging with Sinatra', :with_hostname, :timecop do - let(:io) { StringIO.new } - let(:format) {} + let(:device) { StringIO.new } + let(:last_log_entry) do - io.rewind - JSON.parse(io.read) + device.rewind + JSON.parse(device.read) end + let(:app) do Rack::Builder.new do use Loga::Rack::RequestId use Loga::Rack::Logger - run MySinatraApp + run Loga::MySinatraApp end end before do Loga.reset + Loga.configure( - device: io, + device: device, filter_parameters: [:password], format: format, service_name: 'hello_world_app', @@ -48,80 +56,113 @@ class MySinatraApp < Sinatra::Base ) end - context 'when RACK_ENV is production', if: ENV['RACK_ENV'].eql?('production') do + describe 'RACK_ENV is production', if: ENV['RACK_ENV'].eql?('production') do let(:format) { :gelf } include_examples 'request logger' + it 'attaches a custom context' do + get '/attach_context' + + expect(last_log_entry['_fruit']).to eq 'banana' + end + it 'does not include the controller name and action' do get '/ok' + expect(last_log_entry).not_to include('_request.controller') end end - # rubocop:disable Metrics/LineLength - context 'when RACK_ENV is development', if: ENV['RACK_ENV'].eql?('development') do + describe 'RACK_ENV is development', if: ENV['RACK_ENV'].eql?('development') do let(:format) { :simple } + let(:last_log_entry) do - io.rewind - io.read - end - let(:data) do - { - 'status' => 200, - 'method' => 'GET', - 'path' => '/ok', - 'params' => { 'username'=>'yoshi' }, - 'request_id' => '700a6a01', - 'request_ip' => '127.0.0.1', - 'user_agent' => nil, - 'duration' => 0, - } + device.rewind + device.read end - let(:data_as_text) { "data=#{{ request: data }.inspect}" } + let(:time_pid_tags) { '[2015-12-15T09:30:05.123000+06:00 #999][700a6a01 TEST_TAG]' } - before do - allow(Process).to receive(:pid).and_return(999) - end + before { allow(Process).to receive(:pid).and_return(999) } + + context 'when request responds with HTTP status 200' do + let(:data) do + { + request: { + 'status' => 200, + 'method' => 'GET', + 'path' => '/ok', + 'params' => { 'username'=>'yoshi' }, + 'request_id' => '700a6a01', + 'request_ip' => '127.0.0.1', + 'user_agent' => nil, + 'duration' => 0, + }, + } + end - describe 'get request' do it 'logs the request' do get '/ok', { username: 'yoshi' }, 'HTTP_X_REQUEST_ID' => '700a6a01' - expect(last_log_entry).to eq("I, #{time_pid_tags} GET /ok?username=yoshi 200 in 0ms type=request #{data_as_text}\n") + result = "I, #{time_pid_tags} GET /ok?username=yoshi "\ + "200 in 0ms type=request data=#{data.inspect}\n" + + expect(last_log_entry).to eq(result) end end - describe 'request with redirect' do + context 'when request responds with HTTP status 302' do let(:data) do - super().merge( - 'status' => 302, - 'path' => '/new', - 'params' => {}, - ) + { + request: { + 'status' => 302, + 'method' => 'GET', + 'path' => '/new', + 'params' => {}, + 'request_id' => '700a6a01', + 'request_ip' => '127.0.0.1', + 'user_agent' => nil, + 'duration' => 0, + }, + } end - it 'specifies the original path' do + it 'logs the request' do get '/new', {}, 'HTTP_X_REQUEST_ID' => '700a6a01' - expect(last_log_entry).to eql("I, #{time_pid_tags} GET /new 302 in 0ms type=request #{data_as_text}\n") + + result = "I, #{time_pid_tags} GET /new 302 "\ + "in 0ms type=request data=#{data.inspect}\n" + + expect(last_log_entry).to eql(result) end end - context 'when the request raises an exception' do + context 'when the request responds with HTTP status 500' do let(:data) do - super().merge( - 'status' => 500, - 'path' => '/error', - 'params' => {}, - ) + { + request: { + 'status' => 500, + 'method' => 'GET', + 'path' => '/error', + 'params' => {}, + 'request_id' => '700a6a01', + 'request_ip' => '127.0.0.1', + 'user_agent' => nil, + 'duration' => 0, + }, + } end - it 'logs the request with the exception' do + it 'logs the request' do get '/error', {}, 'HTTP_X_REQUEST_ID' => '700a6a01' - expect(last_log_entry).to eql("E, #{time_pid_tags} GET /error 500 in 0ms type=request #{data_as_text} exception=undefined method `name' for nil:NilClass\n") + + result = "E, #{time_pid_tags} GET /error 500 "\ + "in 0ms type=request data=#{data.inspect} "\ + "exception=undefined method `name' for nil:NilClass\n" + + expect(last_log_entry).to eql(result) end end end - # rubocop:enable Metrics/LineLength end diff --git a/spec/loga/context_manager_spec.rb b/spec/loga/context_manager_spec.rb new file mode 100644 index 0000000..641de40 --- /dev/null +++ b/spec/loga/context_manager_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +RSpec.describe Loga::ContextManager do + subject(:manager) { described_class.new } + + describe '.current' do + it 'returns an instance of context manager' do + expect(described_class.current).to be_an_instance_of(described_class) + end + end + + describe '#clear' do + it 'sets the value of @context to nil' do + manager.clear + + expect(manager.instance_variable_get(:@context)).to eq({}) + end + end + + describe '#attach_context' do + it 'adds the given custom attributes to @context' do + attach_action = -> { manager.attach_context(uuid: 'lorem-ipsum') } + + expect(&attach_action).to change { manager.instance_variable_get(:@context) } + .from({}) + .to(uuid: 'lorem-ipsum') + end + end + + describe '#retrieve_context' do + it 'can retrieve context' do + manager.attach_context(fruit: 'banana') + + expect(manager.retrieve_context).to eq(fruit: 'banana') + end + end +end