contextual_logger 0.9.0 → 0.12.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
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: []