Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TECH-12593:make global_context global, but just for that logger instance #72

Merged
merged 34 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
94f7f24
TECH-12593: store global context as a true global; default to that wh…
ColinDKelley Aug 30, 2023
501d802
TECH-12593: change ContextualLogger to use either Context or Context:…
ColinDKelley Aug 30, 2023
dcb2dcc
TECH-12593: move global_context from process-global to logger-global
ColinDKelley Aug 31, 2023
fff5444
TECH-12593: store global_context in logger; mix Context into LoggerMi…
ColinDKelley Aug 31, 2023
6c23619
TECH-12593: add spec to show that global_context and current_context …
ColinDKelley Aug 31, 2023
de98517
TECH-12593: separate Context from ContextHandler
ColinDKelley Aug 31, 2023
2203516
TECH-12593: bundle update
ColinDKelley Aug 31, 2023
e0f49aa
TECH-12593: add blank line
ColinDKelley Aug 31, 2023
0905e49
TECH-12593: simplify current_context to return nil and current_contex…
ColinDKelley Aug 31, 2023
27b2624
TECH-12593: add spec for .freeze
ColinDKelley Aug 31, 2023
4d3798e
TECH-12593: update CHANGELOG; prep for v1.2.0
ColinDKelley Aug 31, 2023
61552be
TECH-12593: fix LoggerWithContext to delegate global_context
ColinDKelley Aug 31, 2023
5f9f434
TECH-12593: bump version
ColinDKelley Aug 31, 2023
ccd6b3a
TECH-12593: rename [current_context, current_context_for_thread] -> […
ColinDKelley Sep 1, 2023
42446f0
TECH-12593: bump version
ColinDKelley Sep 1, 2023
690d464
TECH-12593: alias current_context_for_thread
ColinDKelley Sep 2, 2023
461c886
TECH-12593: bump version
ColinDKelley Sep 2, 2023
c620d05
use .to change
ColinDKelley Sep 6, 2023
3f514af
TECH-12593: restore code that used context_handler after yield
ColinDKelley Sep 8, 2023
7d1386e
TECH-12593: optimize to skip merge! when empty; test deep merge with …
ColinDKelley Sep 8, 2023
81b2f83
TECH-12593: use ** instead of merge!; keep @deep_merged_context_cache
ColinDKelley Sep 11, 2023
b2d7ba5
TECH-12593: limit @deep_merged_context_cache to 100 entries
ColinDKelley Sep 11, 2023
d3a6971
TECH-12593: remove caching optimizations; add described_class.global_…
ColinDKelley Sep 11, 2023
f2db080
TECH-12593: document Context precedence; refactor LoggerWithContext t…
ColinDKelley Sep 12, 2023
0f92823
TECH-12593: update CHANGELOG
ColinDKelley Sep 12, 2023
6ff10fd
TECH-12593: bump version
ColinDKelley Sep 12, 2023
524c499
TECH-12593: add spec for ContextualLogger::GlobalContextIsLocked
ColinDKelley Sep 12, 2023
7c29ec9
TECH-12593: add trailing newline
ColinDKelley Sep 12, 2023
d4cbaa4
TECH-12593: args -> _args
ColinDKelley Sep 12, 2023
70a4740
TECH-12593: drop redundant self
ColinDKelley Sep 12, 2023
e55ebd2
TECH-12593: remove extra newline
ColinDKelley Sep 12, 2023
80992ab
TECH-12593: move setting ContextualLogger.global_context_lock_message…
ColinDKelley Sep 12, 2023
8597b5c
TECH-12593: bump version
ColinDKelley Sep 12, 2023
1c85c7b
TECH-12593: prep to release v1.2.0
ColinDKelley Sep 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.2.0] - Unreleased
### Changed
- Changed global_context to be stored inside the logger.
- Changed current_context to be stored in a Thread/Fiber-local variable that is unique per instance of the logger.
- Changed `LoggerWithContext` to honor `with_context` on itself and the base logger.

## [1.1.1] - 2022-11-29
### Added
- Bug fix for registering regex
Expand Down Expand Up @@ -99,15 +105,3 @@ are already passed to the formatter as arguments so that the formatter can decid
### Added
- Extracted `ContextualLogger.normalize_log_level` into a public class method so we can call it elsewhere where we allow log_level to be
configured to text values like 'debug'.

[1.0.0]: https://github.com/Invoca/contextual_logger/compare/v0.11.0...v1.0.0
[0.11.0]: https://github.com/Invoca/contextual_logger/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/Invoca/contextual_logger/compare/v0.9.1...v0.10.0
[0.9.1]: https://github.com/Invoca/contextual_logger/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/Invoca/contextual_logger/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/Invoca/contextual_logger/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/Invoca/contextual_logger/compare/v0.6.1...v0.7.0
[0.6.1]: https://github.com/Invoca/contextual_logger/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/Invoca/contextual_logger/compare/v0.5.1...v0.6.0
[0.5.1]: https://github.com/Invoca/contextual_logger/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/Invoca/contextual_logger/compare/v0.4.0...v0.5.0
24 changes: 13 additions & 11 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
PATH
remote: .
specs:
contextual_logger (1.1.1)
contextual_logger (1.2.0.colin.5)
activesupport
json

GEM
remote: https://rubygems.org/
specs:
activesupport (7.0.4.3)
activesupport (7.0.7.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
appraisal (2.4.1)
appraisal (2.5.0)
bundler
rake
thor (>= 0.14.0)
Expand All @@ -29,18 +29,20 @@ GEM
tins (~> 1.6)
diff-lcs (1.5.0)
docile (1.4.0)
i18n (1.12.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
json (2.6.3)
method_source (1.0.0)
minitest (5.18.0)
minitest (5.19.0)
parallel (1.23.0)
parser (3.2.2.0)
parser (3.2.2.3)
ast (~> 2.4.1)
racc
powerpack (0.1.3)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
racc (1.7.1)
rainbow (3.1.1)
rake (13.0.6)
rspec (3.12.0)
Expand All @@ -49,13 +51,13 @@ GEM
rspec-mocks (~> 3.12.0)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.2)
rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.5)
rspec-mocks (3.12.6)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-support (3.12.0)
rspec-support (3.12.1)
rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (0.54.0)
Expand All @@ -79,7 +81,7 @@ GEM
sync (0.5.0)
term-ansicolor (1.7.1)
tins (~> 1.0)
thor (1.2.1)
thor (1.2.2)
tins (1.32.1)
sync
tzinfo (2.0.6)
Expand All @@ -105,4 +107,4 @@ DEPENDENCIES
ruby-prof-flamegraph

BUNDLED WITH
2.2.29
2.3.22
4 changes: 2 additions & 2 deletions contextual_logger.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ Gem::Specification.new do |spec|
"allowed_push_host" => "https://rubygems.org"
}

spec.add_runtime_dependency 'json'
spec.add_runtime_dependency 'activesupport'
spec.add_dependency 'json'
spec.add_dependency 'activesupport'
end
59 changes: 43 additions & 16 deletions lib/contextual_logger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
require 'active_support/core_ext/module/delegation'
require 'json'
require_relative './contextual_logger/redactor'
require_relative './contextual_logger/context/handler'
require_relative './contextual_logger/context'
require_relative './contextual_logger/context_handler'
require_relative './contextual_logger/global_context_lock_message'

module ContextualLogger
LOG_LEVEL_NAMES_TO_SEVERITY =
Expand Down Expand Up @@ -42,32 +44,49 @@ def normalize_message(message)
end
end

# Context Precedence when this is mixed into a logger:
# 1. inline **context passed to the logger method
# 2. `with_context` overrides on the logger object
# 3. `global_context` set on the logger passed to this constructor
module LoggerMixin
include Context

delegate :register_secret, :register_secret_regex, to: :redactor

def global_context
@global_context ||= Context::EMPTY_CONTEXT
end

def global_context=(context)
Context::Handler.new(context).set!
if (global_context_lock_message = ::ContextualLogger.global_context_lock_message)
raise ::ContextualLogger::GlobalContextIsLocked, global_context_lock_message
end
ColinDKelley marked this conversation as resolved.
Show resolved Hide resolved
@global_context = context.freeze
jebentier marked this conversation as resolved.
Show resolved Hide resolved
end

def with_context(context)
context_handler = Context::Handler.new(current_context_for_thread.deep_merge(context))
context_handler.set!
def current_context
current_context_override || global_context
end

# TODO: Deprecate current_context_for_thread in v2.0.
alias current_context_for_thread current_context

def with_context(stacked_context)
context_handler = ContextHandler.new(self, self.current_context_override)
self.current_context_override = deep_merge_with_current_context(stacked_context)

if block_given?
begin
yield
ensure
context_handler.reset!
end
else
# If no block given, the context handler is returned to the caller so they can handle reset! themselves.
# If no block given, return context handler to the caller so they can call reset! themselves.
context_handler
end
end

def current_context_for_thread
Context::Handler.current_context
end

# In the methods generated below, we assume that presence of context means new code that is
# aware of ContextualLogger...and that that code never uses progname.
# This is important because we only get 3 args total (not including &block) passed to `add`,
Expand Down Expand Up @@ -102,7 +121,7 @@ def log_level_enabled?(severity)

# Note that this interface needs to stay compatible with the underlying ::Logger#add interface,
# which is: def add(severity, message = nil, progname = nil)
def add(arg_severity, arg1 = nil, arg2 = nil, **context) # Ruby will prefer to match hashes up to last ** argument
def add(arg_severity, arg1 = nil, arg2 = nil, **context) # Ruby will prefer to match hashes to last argument because of **
severity = arg_severity || UNKNOWN
if log_level_enabled?(severity)
if arg1.nil?
Expand All @@ -117,7 +136,7 @@ def add(arg_severity, arg1 = nil, arg2 = nil, **context) # Ruby will prefer to
message = arg1
progname = arg2 || @progname
end
write_entry_to_log(severity, Time.now, progname, message, context: current_context_for_thread.deep_merge(context))
write_entry_to_log(severity, Time.now, progname, message, context: deep_merge_with_current_context(context))
end

true
Expand All @@ -141,7 +160,7 @@ def format_message(severity, timestamp, progname, message, context: {})
normalized_message = ContextualLogger.normalize_message(message)
normalized_progname = ContextualLogger.normalize_message(progname) unless progname.nil?
if @formatter
@formatter.call(severity, timestamp, normalized_progname, { message: normalized_message }.merge!(context))
@formatter.call(severity, timestamp, normalized_progname, { message: normalized_message, **context })
else
"#{basic_json_log_entry(severity, timestamp, normalized_progname, normalized_message, context: context)}\n"
end
Expand All @@ -151,12 +170,20 @@ def basic_json_log_entry(severity, timestamp, normalized_progname, normalized_me
message_hash = {
message: normalized_progname ? "#{normalized_progname}: #{normalized_message}" : normalized_message,
severity: severity,
timestamp: timestamp
timestamp: timestamp,
**context
}
message_hash[:progname] = normalized_progname if normalized_progname

# merge! is faster and OK here since message_hash is still local only to this method
message_hash.merge!(context).to_json
message_hash.to_json
end

def deep_merge_with_current_context(stacked_context)
if stacked_context.any?
current_context.deep_merge(stacked_context)
else
current_context
end
end
end
end
21 changes: 21 additions & 0 deletions lib/contextual_logger/context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module ContextualLogger
module Context
EMPTY_CONTEXT = {}.freeze

def thread_context_key_for_logger_instance
# We include the object_id here to make these thread/fiber locals unique per logger instance.
@thread_context_key_for_logger_instance ||= "ContextualLogger::Context.context_for_#{object_id}".to_sym
end

def current_context_override
ContextualLogger.global_context_lock_message ||= "ContextualLogger::Context.current_context_override set for #{self.class.name} #{object_id}"
Thread.current[thread_context_key_for_logger_instance]
end

def current_context_override=(context_override)
Thread.current[thread_context_key_for_logger_instance] = context_override.freeze
end
end
end
28 changes: 0 additions & 28 deletions lib/contextual_logger/context/handler.rb

This file was deleted.

14 changes: 14 additions & 0 deletions lib/contextual_logger/context_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module ContextualLogger
class ContextHandler
def initialize(instance, previous_context_override)
@instance = instance
@previous_context_override = previous_context_override
end

def reset!
@instance.current_context_override = @previous_context_override
end
end
end
11 changes: 11 additions & 0 deletions lib/contextual_logger/global_context_lock_message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module ContextualLogger
class << self
attr_accessor :global_context_lock_message # nil or a string indicating what locked the global context
end


class GlobalContextIsLocked < StandardError
end
end
21 changes: 17 additions & 4 deletions lib/contextual_logger/logger_with_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ module ContextualLogger
# A logger that deep_merges additional context and then delegates to the given logger.
# Keeps it own log level (called override_level) that may be set independently of the logger it delegates to.
# If override_level is non-nil, it takes precedence; if it is nil (the default), then it delegates to the logger.
#
# Context Precedence:
# 1. inline **context passed to the logger method
# 2. `with_context` overrides on this LoggerWithContext object
# 3. context passed to this LoggerWithContext constructor
# 4. `with_context` overrides on the logger passed to this constructor
# 5. `global_context` set on the logger passed to this constructor
class LoggerWithContext
include LoggerMixin

Expand All @@ -16,7 +23,13 @@ def initialize(logger, context, level: nil)
@logger = logger
self.level = level
@context = normalize_context(context)
@merged_context_cache = {} # so we don't have to merge every time
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jebentier This is still a bug. I realized it's one of the reasons I had headed down the path of tracking the global_context instance for equals?--that would be necessary in order to fix this bug. But it would be really complicated. And this bug isn't new. The code in this branch works much better in that it handles with_context on both the original logger as well as the LoggerWithContext; it just doesn't handle if you repeat the former after having logged something. That seems very unlikely.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this seems unlikely and that the code required to fix this limitation is more complex than we want for this logger. Thank you for documenting it so we’re aware moving forward.

# TODO: It's a (small) bug that the global_context is memoized at this point. There's a chance that the @logger.current_context
# changes after this because of an enclosing @logger.with_context block. If that happens, we'll miss that extra context.
# The tradeoff is that we don't want to keep calling deep_merge.
def global_context
@global_context ||= @logger.current_context.deep_merge(@context) # this will include any with_context overrides on the `logger`
end

def level
Expand All @@ -29,10 +42,10 @@ def level=(override_level)

def write_entry_to_log(severity, timestamp, progname, message, context:)
merged_context =
if @merged_context_cache.size >= 1000 # keep this cache memory use finite
@merged_context_cache[context] || @context.deep_merge(context)
if context.any?
current_context.deep_merge(context)
else
@merged_context_cache[context] ||= @context.deep_merge(context)
current_context
end

@logger.write_entry_to_log(severity, timestamp, progname, message, context: merged_context)
Expand Down
2 changes: 1 addition & 1 deletion lib/contextual_logger/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module ContextualLogger
VERSION = '1.1.1'
VERSION = '1.2.0.colin.5'
end
32 changes: 0 additions & 32 deletions spec/lib/contextual_logger/context/handler_spec.rb

This file was deleted.

Loading
Loading