contextual_logger 0.9.0 → 0.12.0.pre.1

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: e9f52753d4acdf8c75e8c94154a8002490e88ccc0201bf6623d6b47a2c65791f
4
- data.tar.gz: 304de20b917e9c4078dc83ce96867f44af03d9adbf95e18091e3987e17cabcaa
3
+ metadata.gz: c2ef105e4dfaf32e5e215d83e05ac6ba4f5c67a32cb11e214598a668b4afd34e
4
+ data.tar.gz: d8fa3b949bf2880a1992f6bce1fcfd6b6b59ec4aa46340d9752c8b27fc7dd6cc
5
5
  SHA512:
6
- metadata.gz: 5b3e5489d812a5dc8885a43f92d631a3d99ce3d1a5f7cb631ed16b2457cc2793f4c9ec72d91c09121db047cb83eac3f807db2a722e2481bfc4a39cf85cd12970
7
- data.tar.gz: 78e344d513e2c04ccfa545cc492a514af7d90bf06c335ee80629c10be87339db3004e3fa9d9c801c327d3b4af6af7eec48ece482e05519da7b55cd1a052bd480
6
+ metadata.gz: 3df6d16bf7ffc7cc36bb0f95103222f1791a228ee4e1034ef883261bdaa10ac5d16c57fafa6cdd58d524fee2ebec5c86fab5b2dcc3df05eaf2d35acea1f00eed
7
+ data.tar.gz: e1db9f65be3eb35690cea59ceee623188b40f03377b7a467d12d2644ed8a957246afb70a43eb736a639d541ecdf093d7832f909b0878d1e87c41d889e9be44ea
@@ -5,8 +5,19 @@ require 'active_support/core_ext/module/delegation'
5
5
  require 'json'
6
6
  require_relative './contextual_logger/redactor'
7
7
  require_relative './contextual_logger/context/handler'
8
+ require_relative './contextual_logger/context/registry'
8
9
 
9
10
  module ContextualLogger
11
+ LOG_LEVEL_NAMES_TO_SEVERITY =
12
+ {
13
+ debug: Logger::Severity::DEBUG,
14
+ info: Logger::Severity::INFO,
15
+ warn: Logger::Severity::WARN,
16
+ error: Logger::Severity::ERROR,
17
+ fatal: Logger::Severity::FATAL,
18
+ unknown: Logger::Severity::UNKNOWN
19
+ }.freeze
20
+
10
21
  class << self
11
22
  def new(logger)
12
23
  logger.extend(LoggerMixin)
@@ -17,22 +28,8 @@ module ContextualLogger
17
28
  if log_level.is_a?(Integer) && (Logger::Severity::DEBUG..Logger::Severity::UNKNOWN).include?(log_level)
18
29
  log_level
19
30
  else
20
- case log_level.to_s.downcase
21
- when 'debug'
22
- Logger::Severity::DEBUG
23
- when 'info'
24
- Logger::Severity::INFO
25
- when 'warn'
26
- Logger::Severity::WARN
27
- when 'error'
28
- Logger::Severity::ERROR
29
- when 'fatal'
30
- Logger::Severity::FATAL
31
- when 'unknown'
32
- Logger::Severity::UNKNOWN
33
- else
31
+ LOG_LEVEL_NAMES_TO_SEVERITY[log_level.to_s.downcase.to_sym] or
34
32
  raise ArgumentError, "invalid log level: #{log_level.inspect}"
35
- end
36
33
  end
37
34
  end
38
35
 
@@ -49,12 +46,22 @@ module ContextualLogger
49
46
  module LoggerMixin
50
47
  delegate :register_secret, to: :redactor
51
48
 
49
+ def register_context(&block)
50
+ block or raise ArgumentError, 'Block of context definitions was not passed'
51
+ @context_registry = Context::Registry.new(&block)
52
+ end
53
+
52
54
  def global_context=(context)
53
- Context::Handler.new(context).set!
55
+ Context::Handler.new(context_registry.format(context)).set!
54
56
  end
55
57
 
56
58
  def with_context(context)
57
- context_handler = Context::Handler.new(current_context_for_thread.deep_merge(context))
59
+ context_handler = Context::Handler.new(
60
+ current_context_for_thread.deep_merge(
61
+ context_registry.format(context)
62
+ )
63
+ )
64
+
58
65
  context_handler.set!
59
66
  if block_given?
60
67
  begin
@@ -72,47 +79,56 @@ module ContextualLogger
72
79
  Context::Handler.current_context
73
80
  end
74
81
 
75
- def debug(message = nil, context = {})
76
- add(Logger::Severity::DEBUG, message.nil? && block_given? ? yield : message, **context)
77
- end
78
-
79
- def info(message = nil, context = {})
80
- add(Logger::Severity::INFO, message.nil? && block_given? ? yield : message, **context)
81
- end
82
+ # In the methods generated below, we assume that presence of context means new code that is
83
+ # aware of ContextualLogger...and that that code never uses progname.
84
+ # This is important because we only get 3 args total (not including &block) passed to `add`,
85
+ # in order to be compatible with classic implementations like in the plain ::Logger and
86
+ # ActiveSupport::Logger.broadcast.
82
87
 
83
- def warn(message = nil, context = {})
84
- add(Logger::Severity::WARN, message.nil? && block_given? ? yield : message, **context)
85
- end
88
+ # Note that we can't yield before `add` because `add` might skip it based on log_level. And we can't check
89
+ # log_level here because we might be running in ActiveSupport::Logging.broadcast which has multiple
90
+ # loggers, each with their own log_level.
86
91
 
87
- def error(message = nil, context = {})
88
- add(Logger::Severity::ERROR, message.nil? && block_given? ? yield : message, **context)
89
- end
90
-
91
- def fatal(message = nil, context = {})
92
- add(Logger::Severity::FATAL, message.nil? && block_given? ? yield : message, **context)
93
- end
94
-
95
- def unknown(message = nil, context = {})
96
- add(Logger::Severity::UNKNOWN, message.nil? && block_given? ? yield : message, **context)
92
+ LOG_LEVEL_NAMES_TO_SEVERITY.each do |method_name, log_level|
93
+ class_eval(<<~EOS, __FILE__, __LINE__ + 1)
94
+ def #{method_name}(arg = nil, **context, &block)
95
+ if context.empty?
96
+ add(#{log_level}, nil, arg, &block)
97
+ else
98
+ if arg.nil?
99
+ add(#{log_level}, nil, context, &block)
100
+ elsif block
101
+ add(#{log_level}, nil, context.merge(progname: arg), &block)
102
+ else
103
+ add(#{log_level}, arg, context)
104
+ end
105
+ end
106
+ end
107
+ EOS
97
108
  end
98
109
 
99
110
  def log_level_enabled?(severity)
100
111
  severity >= level
101
112
  end
102
113
 
103
- def add(init_severity, message = nil, init_progname = nil, **context) # Ruby will prefer to match hashes up to last ** argument
104
- severity = init_severity || UNKNOWN
114
+ # Note that this interface needs to stay compatible with the underlying ::Logger#add interface,
115
+ # which is: def add(severity, message = nil, progname = nil)
116
+ def add(arg_severity, arg1 = nil, arg2 = nil, **context) # Ruby will prefer to match hashes up to last ** argument
117
+ severity = arg_severity || UNKNOWN
105
118
  if log_level_enabled?(severity)
106
- progname = init_progname || @progname
107
- if message.nil?
119
+ if arg1.nil?
108
120
  if block_given?
109
121
  message = yield
122
+ progname = arg2 || context.delete(:progname) || @progname
110
123
  else
111
- message = init_progname
124
+ message = arg2
112
125
  progname = @progname
113
126
  end
127
+ else
128
+ message = arg1
129
+ progname = arg2 || @progname
114
130
  end
115
- write_entry_to_log(severity, Time.now, progname, message, context: current_context_for_thread.deep_merge(context))
131
+ write_entry_to_log(severity, Time.now, progname, message, context: current_context_for_thread.deep_merge(context_registry.format(context)))
116
132
  end
117
133
 
118
134
  true
@@ -128,27 +144,36 @@ module ContextualLogger
128
144
 
129
145
  private
130
146
 
147
+ def context_registry
148
+ @context_registry ||= Context::Registry.new do
149
+ strict false
150
+ raise_on_missing_definition false
151
+ end
152
+ end
153
+
131
154
  def redactor
132
155
  @redactor ||= Redactor.new
133
156
  end
134
157
 
135
158
  def format_message(severity, timestamp, progname, message, context: {})
159
+ normalized_message = ContextualLogger.normalize_message(message)
160
+ normalized_progname = ContextualLogger.normalize_message(progname) unless progname.nil?
136
161
  if @formatter
137
- @formatter.call(severity, timestamp, progname, { message: ContextualLogger.normalize_message(message), **context })
162
+ @formatter.call(severity, timestamp, normalized_progname, { message: normalized_message }.merge!(context))
138
163
  else
139
- "#{basic_json_log_entry(severity, timestamp, progname, message, context: context)}\n"
164
+ "#{basic_json_log_entry(severity, timestamp, normalized_progname, normalized_message, context: context)}\n"
140
165
  end
141
166
  end
142
167
 
143
- def basic_json_log_entry(severity, timestamp, progname, message, context:)
168
+ def basic_json_log_entry(severity, timestamp, normalized_progname, normalized_message, context:)
144
169
  message_hash = {
145
- message: ContextualLogger.normalize_message(message),
170
+ message: normalized_progname ? "#{normalized_progname}: #{normalized_message}" : normalized_message,
146
171
  severity: severity,
147
172
  timestamp: timestamp
148
173
  }
149
- message_hash[:progname] = progname if progname
174
+ message_hash[:progname] = normalized_progname if normalized_progname
150
175
 
151
- # using merge! instead of merge for speed of operation
176
+ # merge! is faster and OK here since message_hash is still local only to this method
152
177
  message_hash.merge!(context).to_json
153
178
  end
154
179
  end
@@ -7,8 +7,10 @@ module ContextualLogger
7
7
 
8
8
  attr_reader :previous_context, :context
9
9
 
10
- def self.current_context
11
- Thread.current[THREAD_CONTEXT_NAMESPACE] || {}
10
+ class << self
11
+ def current_context
12
+ Thread.current[THREAD_CONTEXT_NAMESPACE] || {}
13
+ end
12
14
  end
13
15
 
14
16
  def initialize(context, previous_context: nil)
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'registry_types/string'
4
+ require_relative 'registry_types/boolean'
5
+ require_relative 'registry_types/number'
6
+ require_relative 'registry_types/date'
7
+ require_relative 'registry_types/hash'
8
+
9
+ # This class is responsible for holding the registered context shape that will
10
+ # be used by the LoggerMixin to make sure that the context matches the shape
11
+ # defined
12
+
13
+ # logger.configure_context do
14
+ # strict false
15
+ #
16
+ # string test_string
17
+ # integer test_integer
18
+ # hash :test_hash do
19
+ # string :test_string_in_hash
20
+ # date :date_time_in_hash
21
+ # end
22
+ # end
23
+
24
+ module ContextualLogger
25
+ module Context
26
+ class Registry < RegistryTypes::Hash
27
+ class DuplicateDefinitionError < StandardError; end
28
+ class MissingDefinitionError < StandardError; end
29
+
30
+ def initialize(&definitions)
31
+ @strict = true
32
+ @raise_on_missing_definition = true
33
+
34
+ super
35
+ end
36
+
37
+ def strict?
38
+ @strict
39
+ end
40
+
41
+ def raise_on_missing_definition?
42
+ @raise_on_missing_definition
43
+ end
44
+
45
+ def format(context)
46
+ if strict?
47
+ super(context, raise_on_missing_definition?)
48
+ else
49
+ context
50
+ end
51
+ end
52
+
53
+ alias context_shape to_h
54
+
55
+ def to_h
56
+ {
57
+ strict: @strict,
58
+ context_shape: context_shape
59
+ }
60
+ end
61
+
62
+ private
63
+
64
+ def strict(value)
65
+ @strict = value
66
+ end
67
+
68
+ def raise_on_missing_definition(value)
69
+ @raise_on_missing_definition = value
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContextualLogger
4
+ module Context
5
+ module RegistryTypes
6
+ class Boolean
7
+ attr_reader :formatter
8
+
9
+ def initialize(formatter: nil)
10
+ @formatter = formatter || ->(value) { value ? true : false }
11
+ end
12
+
13
+ def to_h
14
+ { type: :boolean, formatter: formatter }
15
+ end
16
+
17
+ def format(value)
18
+ case formatter
19
+ when Proc
20
+ formatter.call(value)
21
+ else
22
+ value.send(formatter)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContextualLogger
4
+ module Context
5
+ module RegistryTypes
6
+ class Date
7
+ attr_reader :formatter
8
+
9
+ def initialize(formatter: nil)
10
+ @formatter = formatter || ->(value) { value.iso8601(6) }
11
+ end
12
+
13
+ def to_h
14
+ { type: :date, formatter: formatter }
15
+ end
16
+
17
+ def format(value)
18
+ case formatter
19
+ when Proc
20
+ formatter.call(value)
21
+ else
22
+ value.send(formatter)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'string'
4
+ require_relative 'boolean'
5
+ require_relative 'number'
6
+ require_relative 'date'
7
+
8
+ module ContextualLogger
9
+ module Context
10
+ module RegistryTypes
11
+ class Hash
12
+ def initialize(&definitions)
13
+ @definitions = {}
14
+
15
+ run(&definitions)
16
+ end
17
+
18
+ def to_h
19
+ @definitions.reduce({}) do |shape_hash, (key, value)|
20
+ shape_hash.merge(key => value.to_h)
21
+ end
22
+ end
23
+
24
+ def format(context, raise_on_missing_definition)
25
+ context.reduce({}) do |formatted_context, (key, value)|
26
+ if (definition = @definitions[key])
27
+ formatted_context[key] = if definition.is_a?(RegistryTypes::Hash)
28
+ definition.format(value, raise_on_missing_definition)
29
+ else
30
+ definition.format(value)
31
+ end
32
+ elsif raise_on_missing_definition
33
+ raise Registry::MissingDefinitionError, "Attempting to apply context #{key} that is missing a definition in the registry"
34
+ end
35
+
36
+ formatted_context
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def string(context_key, formatter: nil)
43
+ dedup(context_key, :string)
44
+ @definitions[context_key] = RegistryTypes::String.new(formatter: formatter)
45
+ end
46
+
47
+ def boolean(context_key, formatter: nil)
48
+ dedup(context_key, :boolean)
49
+ @definitions[context_key] = RegistryTypes::Boolean.new(formatter: formatter)
50
+ end
51
+
52
+ def number(context_key, formatter: nil)
53
+ dedup(context_key, :number)
54
+ @definitions[context_key] = RegistryTypes::Number.new(formatter: formatter)
55
+ end
56
+
57
+ def date(context_key, formatter: nil)
58
+ dedup(context_key, :date)
59
+ @definitions[context_key] = RegistryTypes::Date.new(formatter: formatter)
60
+ end
61
+
62
+ def hash(context_key, &definitions)
63
+ dedup(context_key, :hash)
64
+ @definitions[context_key] = RegistryTypes::Hash.new(&definitions)
65
+ end
66
+
67
+ def run(&definitions)
68
+ instance_eval(&definitions)
69
+ end
70
+
71
+ def dedup(key, type)
72
+ @definitions.include?(key) and
73
+ raise Registry::DuplicateDefinitionError, "Defining duplicate entry #{key} previously as #{@definitions[key]} and now as #{type}"
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContextualLogger
4
+ module Context
5
+ module RegistryTypes
6
+ class Number
7
+ attr_reader :formatter
8
+
9
+ def initialize(formatter: nil)
10
+ @formatter = formatter || :to_i
11
+ end
12
+
13
+ def to_h
14
+ { type: :number, formatter: formatter }
15
+ end
16
+
17
+ def format(value)
18
+ case formatter
19
+ when Proc
20
+ formatter.call(value)
21
+ else
22
+ value.send(formatter)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContextualLogger
4
+ module Context
5
+ module RegistryTypes
6
+ class String
7
+ attr_reader :formatter
8
+
9
+ def initialize(formatter: nil)
10
+ @formatter = formatter || :to_s
11
+ end
12
+
13
+ def to_h
14
+ { type: :string, formatter: formatter }
15
+ end
16
+
17
+ def format(value)
18
+ case formatter
19
+ when Proc
20
+ formatter.call(value)
21
+ else
22
+ value.send(formatter)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext/hash/keys'
4
+
3
5
  module ContextualLogger
4
6
  # A logger that deep_merges additional context and then delegates to the given logger.
5
7
  # Keeps it own log level (called override_level) that may be set independently of the logger it delegates to.
@@ -13,7 +15,7 @@ module ContextualLogger
13
15
  logger.is_a?(LoggerMixin) or raise ArgumentError, "logger must include ContextualLogger::LoggerMixin (got #{logger.inspect})"
14
16
  @logger = logger
15
17
  self.level = level
16
- @context = context
18
+ @context = normalize_context(context)
17
19
  @merged_context_cache = {} # so we don't have to merge every time
18
20
  end
19
21
 
@@ -36,6 +38,29 @@ module ContextualLogger
36
38
  @logger.write_entry_to_log(severity, timestamp, progname, message, context: merged_context)
37
39
  end
38
40
 
41
+ private
42
+
43
+ def normalize_context(context)
44
+ if warn_on_string_keys(context)
45
+ context.deep_symbolize_keys
46
+ else
47
+ context
48
+ end
49
+ end
50
+
51
+ def warn_on_string_keys(context)
52
+ if deep_key_has_string?(context)
53
+ ActiveSupport::Deprecation.warn('Context keys must use symbols not strings. This will be asserted as of contextual_logger v1.0.0')
54
+ end
55
+ end
56
+
57
+ def deep_key_has_string?(hash)
58
+ hash.any? do |key, value|
59
+ key.is_a?(String) ||
60
+ (value.is_a?(Hash) && deep_key_has_string?(value))
61
+ end
62
+ end
63
+
39
64
  class << self
40
65
  def for_log_source(logger, log_source, level: nil)
41
66
  new(logger, { log_source: log_source }, level: level)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ContextualLogger
4
- VERSION = '0.9.0'
4
+ VERSION = '0.12.0.pre.1'
5
5
  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: 0.9.0
4
+ version: 0.12.0.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Ebentier
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-17 00:00:00.000000000 Z
11
+ date: 2020-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -46,6 +46,12 @@ extra_rdoc_files: []
46
46
  files:
47
47
  - lib/contextual_logger.rb
48
48
  - lib/contextual_logger/context/handler.rb
49
+ - lib/contextual_logger/context/registry.rb
50
+ - lib/contextual_logger/context/registry_types/boolean.rb
51
+ - lib/contextual_logger/context/registry_types/date.rb
52
+ - lib/contextual_logger/context/registry_types/hash.rb
53
+ - lib/contextual_logger/context/registry_types/number.rb
54
+ - lib/contextual_logger/context/registry_types/string.rb
49
55
  - lib/contextual_logger/logger_with_context.rb
50
56
  - lib/contextual_logger/overrides/active_support/tagged_logging/formatter.rb
51
57
  - lib/contextual_logger/redactor.rb
@@ -56,7 +62,7 @@ licenses:
56
62
  metadata:
57
63
  source_code_uri: https://github.com/Invoca/contextual_logger
58
64
  allowed_push_host: https://rubygems.org
59
- post_install_message:
65
+ post_install_message:
60
66
  rdoc_options: []
61
67
  require_paths:
62
68
  - lib
@@ -67,12 +73,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
67
73
  version: '0'
68
74
  required_rubygems_version: !ruby/object:Gem::Requirement
69
75
  requirements:
70
- - - ">="
76
+ - - ">"
71
77
  - !ruby/object:Gem::Version
72
- version: '0'
78
+ version: 1.3.1
73
79
  requirements: []
74
80
  rubygems_version: 3.0.3
75
- signing_key:
81
+ signing_key:
76
82
  specification_version: 4
77
83
  summary: Add context to your logger
78
84
  test_files: []