contextual_logger 1.2.0.pre.1 → 1.3.0.dg.pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5ee8cad9d62ca2d8aeec81f2626b56b4aa6b1db2c6b626518a4708d590ddb4c
4
- data.tar.gz: 71be08f271be68de04f942abfa301a2a5ec54be54d0399bb01db8bcf52a07037
3
+ metadata.gz: 96fa2fd042325883674d751a6b2443c23e9e6b195f9f15ade2826832c6557735
4
+ data.tar.gz: 180923158cdaac1c7cd63755ca93e4cda42aaf48bab442b21981264a255a5765
5
5
  SHA512:
6
- metadata.gz: 2b455d41a46223ea9c6e2583a7d03d81940743215c4eb1dd5977d6ffc8f8ffcb618ce27a3b975fed211987b592e0cfdbc1e691ceef155641764c5d8938da0679
7
- data.tar.gz: 6600b357b1dc7b25ee8e746f17280a7e74ac184b38a03340f48e835659259135321a0c0ff0d7c387a7b06b9804b82ad63f8b76a911790a6fb5c2f99a61c0cfdc
6
+ metadata.gz: 66081a0a05dded370cf77b2e60b2c20884e8993a362bf6be4674b3c4b0c95d4811c208b8b6ab64ea99495fc7ca05b16ba9ef779cb39f4169620a06eac0c12fce
7
+ data.tar.gz: 04d1997d713eec2231a08e2085728db8ef438268835b4ac6e3eb866ec01c7d5df0447677de127c286236aa0485d4e233b36214234aa661cd0c30c6fe52eaaef8
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContextualLogger
4
+ module Context
5
+ EMPTY_CONTEXT = {}.freeze
6
+
7
+ def thread_context_key_for_logger_instance
8
+ # We include the object_id here to make these thread/fiber locals unique per logger instance.
9
+ @thread_context_key_for_logger_instance ||= "ContextualLogger::Context.context_for_#{object_id}".to_sym
10
+ end
11
+
12
+ def current_context_override
13
+ Thread.current[thread_context_key_for_logger_instance]
14
+ end
15
+
16
+ def current_context_override=(context_override)
17
+ ContextualLogger.global_context_lock_message ||= "ContextualLogger::Context.current_context_override set for #{self.class.name} #{object_id}: #{context_override.inspect}"
18
+ Thread.current[thread_context_key_for_logger_instance] = context_override.freeze
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContextualLogger
4
+ class ContextHandler
5
+ def initialize(instance, previous_context_override)
6
+ @instance = instance
7
+ @previous_context_override = previous_context_override
8
+ end
9
+
10
+ def reset!
11
+ @instance.current_context_override = @previous_context_override
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContextualLogger
4
+ class << self
5
+ attr_accessor :global_context_lock_message # nil or a string indicating what locked the global context
6
+ end
7
+
8
+ class GlobalContextIsLocked < StandardError
9
+ end
10
+ end
@@ -6,6 +6,13 @@ module ContextualLogger
6
6
  # A logger that deep_merges additional context and then delegates to the given logger.
7
7
  # Keeps it own log level (called override_level) that may be set independently of the logger it delegates to.
8
8
  # If override_level is non-nil, it takes precedence; if it is nil (the default), then it delegates to the logger.
9
+ #
10
+ # Context Precedence:
11
+ # 1. inline **context passed to the logger method
12
+ # 2. `with_context` overrides on this LoggerWithContext object
13
+ # 3. context passed to this LoggerWithContext constructor
14
+ # 4. `with_context` overrides on the logger passed to this constructor
15
+ # 5. `global_context` set on the logger passed to this constructor
9
16
  class LoggerWithContext
10
17
  include LoggerMixin
11
18
 
@@ -16,7 +23,13 @@ module ContextualLogger
16
23
  @logger = logger
17
24
  self.level = level
18
25
  @context = normalize_context(context)
19
- @merged_context_cache = {} # so we don't have to merge every time
26
+ end
27
+
28
+ # TODO: It's a (small) bug that the global_context is memoized at this point. There's a chance that the @logger.current_context
29
+ # changes after this because of an enclosing @logger.with_context block. If that happens, we'll miss that extra context.
30
+ # The tradeoff is that we don't want to keep calling deep_merge.
31
+ def global_context
32
+ @global_context ||= @logger.current_context.deep_merge(@context) # this will include any with_context overrides on the `logger`
20
33
  end
21
34
 
22
35
  def level
@@ -29,10 +42,10 @@ module ContextualLogger
29
42
 
30
43
  def write_entry_to_log(severity, timestamp, progname, message, context:)
31
44
  merged_context =
32
- if @merged_context_cache.size >= 1000 # keep this cache memory use finite
33
- @merged_context_cache[context] || @context.deep_merge(context)
45
+ if context.any?
46
+ current_context.deep_merge(context)
34
47
  else
35
- @merged_context_cache[context] ||= @context.deep_merge(context)
48
+ current_context
36
49
  end
37
50
 
38
51
  @logger.write_entry_to_log(severity, timestamp, progname, message, context: merged_context)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ContextualLogger
4
- VERSION = '1.2.0.pre.1'
4
+ VERSION = '1.3.0.dg.pre.0'
5
5
  end
@@ -4,7 +4,9 @@ require 'active_support'
4
4
  require 'active_support/core_ext/module/delegation'
5
5
  require 'json'
6
6
  require_relative './contextual_logger/redactor'
7
- require_relative './contextual_logger/context/handler'
7
+ require_relative './contextual_logger/context'
8
+ require_relative './contextual_logger/context_handler'
9
+ require_relative './contextual_logger/global_context_lock_message'
8
10
 
9
11
  module ContextualLogger
10
12
  LOG_LEVEL_NAMES_TO_SEVERITY =
@@ -17,6 +19,9 @@ module ContextualLogger
17
19
  unknown: Logger::Severity::UNKNOWN
18
20
  }.freeze
19
21
 
22
+ class LambdaAlreadyDefinedError < StandardError
23
+ end
24
+
20
25
  class << self
21
26
  def new(logger)
22
27
  logger.extend(LoggerMixin)
@@ -42,16 +47,57 @@ module ContextualLogger
42
47
  end
43
48
  end
44
49
 
50
+ # Context Precedence when this is mixed into a logger:
51
+ # 1. inline **context passed to the logger method
52
+ # 2. `with_context` overrides on the logger object
53
+ # 3. `global_context` set on the logger passed to this constructor
45
54
  module LoggerMixin
55
+ include Context
56
+
46
57
  delegate :register_secret, :register_secret_regex, to: :redactor
47
58
 
59
+ def global_context
60
+ @global_context ||= Context::EMPTY_CONTEXT
61
+ end
62
+
63
+ def add_global_context_lambda(field, lambda)
64
+ if field.blank?
65
+ raise ArgumentError, "The field cannot be empty"
66
+ end
67
+
68
+ unless lambda.respond_to?(:call)
69
+ raise ArgumentError, "A lambda must respond to the :call method"
70
+ end
71
+
72
+ if global_context_lambdas[field]
73
+ raise ::ContextualLogger::LambdaAlreadyDefinedError, "A lambda for `#{field}` is already defined"
74
+ end
75
+
76
+ @global_context_lambdas[field] = lambda
77
+ end
78
+
79
+ def global_context_lambdas
80
+ @global_context_lambdas ||= {}
81
+ end
82
+
48
83
  def global_context=(context)
49
- Context::Handler.new(context).set!
84
+ if (global_context_lock_message = ::ContextualLogger.global_context_lock_message)
85
+ raise ::ContextualLogger::GlobalContextIsLocked, global_context_lock_message
86
+ end
87
+ @global_context = context.freeze
88
+ end
89
+
90
+ def current_context
91
+ current_context_override || global_context
50
92
  end
51
93
 
52
- def with_context(context)
53
- context_handler = Context::Handler.new(current_context_for_thread.deep_merge(context))
54
- context_handler.set!
94
+ # TODO: Deprecate current_context_for_thread in v2.0.
95
+ alias current_context_for_thread current_context
96
+
97
+ def with_context(stacked_context)
98
+ context_handler = ContextHandler.new(self, current_context_override)
99
+ self.current_context_override = deep_merge_with_current_context(stacked_context)
100
+
55
101
  if block_given?
56
102
  begin
57
103
  yield
@@ -59,15 +105,11 @@ module ContextualLogger
59
105
  context_handler.reset!
60
106
  end
61
107
  else
62
- # If no block given, the context handler is returned to the caller so they can handle reset! themselves.
108
+ # If no block given, return context handler to the caller so they can call reset! themselves.
63
109
  context_handler
64
110
  end
65
111
  end
66
112
 
67
- def current_context_for_thread
68
- Context::Handler.current_context
69
- end
70
-
71
113
  # In the methods generated below, we assume that presence of context means new code that is
72
114
  # aware of ContextualLogger...and that that code never uses progname.
73
115
  # This is important because we only get 3 args total (not including &block) passed to `add`,
@@ -102,7 +144,7 @@ module ContextualLogger
102
144
 
103
145
  # Note that this interface needs to stay compatible with the underlying ::Logger#add interface,
104
146
  # which is: def add(severity, message = nil, progname = nil)
105
- def add(arg_severity, arg1 = nil, arg2 = nil, **context) # Ruby will prefer to match hashes up to last ** argument
147
+ def add(arg_severity, arg1 = nil, arg2 = nil, **context) # Ruby will prefer to match hashes to last argument because of **
106
148
  severity = arg_severity || UNKNOWN
107
149
  if log_level_enabled?(severity)
108
150
  if arg1.nil?
@@ -117,7 +159,8 @@ module ContextualLogger
117
159
  message = arg1
118
160
  progname = arg2 || @progname
119
161
  end
120
- write_entry_to_log(severity, Time.now, progname, message, context: current_context_for_thread.deep_merge(context))
162
+ full_context = evaluate_global_context_lambdas(deep_merge_with_current_context(context))
163
+ write_entry_to_log(severity, Time.now, progname, message, context: full_context)
121
164
  end
122
165
 
123
166
  true
@@ -141,7 +184,7 @@ module ContextualLogger
141
184
  normalized_message = ContextualLogger.normalize_message(message)
142
185
  normalized_progname = ContextualLogger.normalize_message(progname) unless progname.nil?
143
186
  if @formatter
144
- @formatter.call(severity, timestamp, normalized_progname, { message: normalized_message }.merge!(context))
187
+ @formatter.call(severity, timestamp, normalized_progname, { message: normalized_message, **context })
145
188
  else
146
189
  "#{basic_json_log_entry(severity, timestamp, normalized_progname, normalized_message, context: context)}\n"
147
190
  end
@@ -151,12 +194,28 @@ module ContextualLogger
151
194
  message_hash = {
152
195
  message: normalized_progname ? "#{normalized_progname}: #{normalized_message}" : normalized_message,
153
196
  severity: severity,
154
- timestamp: timestamp
197
+ timestamp: timestamp,
198
+ **context
155
199
  }
156
200
  message_hash[:progname] = normalized_progname if normalized_progname
157
201
 
158
- # merge! is faster and OK here since message_hash is still local only to this method
159
- message_hash.merge!(context).to_json
202
+ message_hash.to_json
203
+ end
204
+
205
+ def deep_merge_with_current_context(stacked_context)
206
+ if stacked_context.any?
207
+ current_context.deep_merge(stacked_context)
208
+ else
209
+ current_context
210
+ end
211
+ end
212
+
213
+ def evaluate_global_context_lambdas(context)
214
+ if global_context_lambdas.empty?
215
+ context
216
+ else
217
+ global_context_lambdas.transform_values { |lambda| lambda.call }.merge(context)
218
+ end
160
219
  end
161
220
  end
162
221
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contextual_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0.pre.1
4
+ version: 1.3.0.dg.pre.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Ebentier
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-09 00:00:00.000000000 Z
11
+ date: 2023-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -45,7 +45,9 @@ extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
47
  - lib/contextual_logger.rb
48
- - lib/contextual_logger/context/handler.rb
48
+ - lib/contextual_logger/context.rb
49
+ - lib/contextual_logger/context_handler.rb
50
+ - lib/contextual_logger/global_context_lock_message.rb
49
51
  - lib/contextual_logger/logger_with_context.rb
50
52
  - lib/contextual_logger/overrides/active_support/tagged_logging/formatter.rb
51
53
  - lib/contextual_logger/redactor.rb
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ContextualLogger
4
- module Context
5
- class Handler
6
- THREAD_CONTEXT_NAMESPACE = 'ContextualLoggerCurrentLoggingContext'
7
-
8
- attr_reader :previous_context, :context
9
-
10
- def self.current_context
11
- Thread.current[THREAD_CONTEXT_NAMESPACE] || {}
12
- end
13
-
14
- def initialize(context, previous_context: nil)
15
- @previous_context = previous_context || self.class.current_context
16
- @context = context
17
- end
18
-
19
- def set!
20
- Thread.current[THREAD_CONTEXT_NAMESPACE] = context
21
- end
22
-
23
- def reset!
24
- Thread.current[THREAD_CONTEXT_NAMESPACE] = previous_context
25
- end
26
- end
27
- end
28
- end