lumberjack 2.0.5 → 2.1.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +0 -17
  3. data/CHANGELOG.md +52 -0
  4. data/README.md +35 -0
  5. data/UPGRADE_GUIDE.md +1 -1
  6. data/VERSION +1 -1
  7. data/lib/lumberjack/attribute_formatter.rb +0 -51
  8. data/lib/lumberjack/context.rb +6 -1
  9. data/lib/lumberjack/context_locals.rb +10 -2
  10. data/lib/lumberjack/context_logger.rb +14 -4
  11. data/lib/lumberjack/device/buffer.rb +52 -37
  12. data/lib/lumberjack/device/multi.rb +2 -2
  13. data/lib/lumberjack/device/test.rb +70 -13
  14. data/lib/lumberjack/device/writer.rb +35 -12
  15. data/lib/lumberjack/device.rb +0 -2
  16. data/lib/lumberjack/device_registry.rb +7 -6
  17. data/lib/lumberjack/entry_formatter.rb +4 -2
  18. data/lib/lumberjack/formatter/structured_formatter.rb +4 -4
  19. data/lib/lumberjack/formatter/tags_formatter.rb +3 -3
  20. data/lib/lumberjack/formatter.rb +0 -11
  21. data/lib/lumberjack/formatter_registry.rb +7 -6
  22. data/lib/lumberjack/log_entry.rb +13 -46
  23. data/lib/lumberjack/log_entry_matcher/indifferent_hash.rb +83 -0
  24. data/lib/lumberjack/log_entry_matcher/score.rb +12 -4
  25. data/lib/lumberjack/log_entry_matcher.rb +229 -28
  26. data/lib/lumberjack/logger.rb +19 -185
  27. data/lib/lumberjack/template.rb +65 -42
  28. data/lib/lumberjack/template_registry.rb +7 -6
  29. data/lib/lumberjack/utils.rb +1 -26
  30. data/lib/lumberjack.rb +5 -25
  31. data/lumberjack.gemspec +0 -2
  32. metadata +3 -22
  33. data/lib/lumberjack/device/date_rolling_log_file.rb +0 -22
  34. data/lib/lumberjack/device/size_rolling_log_file.rb +0 -22
  35. data/lib/lumberjack/formatter/tagged_message.rb +0 -17
  36. data/lib/lumberjack/tag_context.rb +0 -15
  37. data/lib/lumberjack/tag_formatter.rb +0 -31
  38. data/lib/lumberjack/tags.rb +0 -36
@@ -54,7 +54,11 @@ module Lumberjack
54
54
  # @option options [Boolean] :colorize (false) Whether to colorize log output
55
55
  def initialize(stream, options = {})
56
56
  @stream = stream
57
- @stream.sync = true if @stream.respond_to?(:sync=) && options[:autoflush] != false
57
+ @autoflush = options[:autoflush] != false
58
+ # Only turn sync on; the stream may be shared with other code (i.e. $stdout), so
59
+ # disabling autoflush must not change how anything else writing to it behaves.
60
+ @stream.sync = true if @autoflush && @stream.respond_to?(:sync=)
61
+ @lock = Mutex.new
58
62
 
59
63
  @binmode = options[:binmode]
60
64
 
@@ -107,8 +111,10 @@ module Lumberjack
107
111
  #
108
112
  # @return [void]
109
113
  def close
114
+ # Call the public flush so that subclasses that override it still get a chance to
115
+ # drain whatever they buffer. It must be called outside the lock since it takes it.
110
116
  flush
111
- stream.close
117
+ @lock.synchronize { stream.close }
112
118
  end
113
119
 
114
120
  # Flush the underlying stream to ensure all buffered data is written to the
@@ -117,7 +123,9 @@ module Lumberjack
117
123
  #
118
124
  # @return [void]
119
125
  def flush
120
- stream.flush if stream.respond_to?(:flush)
126
+ @lock.synchronize do
127
+ stream.flush if stream.respond_to?(:flush)
128
+ end
121
129
  end
122
130
 
123
131
  # Get the current datetime format from the template if supported. Returns the
@@ -164,7 +172,20 @@ module Lumberjack
164
172
  # The underlying stream object that is being written to.
165
173
  #
166
174
  # @return [IO] The current stream object
167
- attr_accessor :stream
175
+ attr_reader :stream
176
+
177
+ # Replace the underlying stream. The swap is synchronized against in-flight
178
+ # writes so a log line is never split across the old and new streams, and the
179
+ # autoflush setting is re-applied to the new stream.
180
+ #
181
+ # @param value [IO, #write] The new stream to write to
182
+ # @return [void]
183
+ def stream=(value)
184
+ @lock.synchronize do
185
+ value.sync = true if @autoflush && value.respond_to?(:sync=)
186
+ @stream = value
187
+ end
188
+ end
168
189
 
169
190
  private
170
191
 
@@ -177,16 +198,18 @@ module Lumberjack
177
198
  def write_to_stream(line)
178
199
  out = line.end_with?(Lumberjack::LINE_SEPARATOR) ? line : "#{line}#{Lumberjack::LINE_SEPARATOR}"
179
200
  begin
180
- begin
181
- stream.write(out)
182
- rescue IOError => e
183
- raise e if stream.closed?
184
-
185
- stream.write(out)
201
+ @lock.synchronize do
202
+ target = stream
203
+ begin
204
+ target.write(out)
205
+ rescue IOError => e
206
+ raise e if target.closed?
207
+
208
+ target.write(out)
209
+ end
186
210
  end
187
211
  rescue => e
188
- $stderr.write(error_message(e))
189
- $stderr.write(out)
212
+ $stderr.write("#{error_message(e)}#{out}")
190
213
  end
191
214
  end
192
215
 
@@ -32,8 +32,6 @@ module Lumberjack
32
32
  require_relative "device/null"
33
33
  require_relative "device/test"
34
34
  require_relative "device/buffer"
35
- require_relative "device/size_rolling_log_file"
36
- require_relative "device/date_rolling_log_file"
37
35
 
38
36
  class << self
39
37
  # Open a logging device with the given options.
@@ -17,6 +17,7 @@ module Lumberjack
17
17
  # logger = Lumberjack::Logger.new(:my_device)
18
18
  module DeviceRegistry
19
19
  @registry = {stdout: :stdout, stderr: :stderr}
20
+ @lock = Mutex.new
20
21
 
21
22
  class << self
22
23
  # Register a device name. Device names can be used to associate a symbol with a device
@@ -31,7 +32,7 @@ module Lumberjack
31
32
  def add(name, klass)
32
33
  raise ArgumentError.new("name must be a symbol") unless name.is_a?(Symbol)
33
34
 
34
- @registry[name] = klass
35
+ @lock.synchronize { @registry[name] = klass }
35
36
  end
36
37
 
37
38
  # Remove a device from the registry.
@@ -39,7 +40,7 @@ module Lumberjack
39
40
  # @param name [Symbol] The name of the device to remove
40
41
  # @return [void]
41
42
  def remove(name)
42
- @registry.delete(name)
43
+ @lock.synchronize { @registry.delete(name) }
43
44
  end
44
45
 
45
46
  # Check if a device is registered.
@@ -47,7 +48,7 @@ module Lumberjack
47
48
  # @param name [Symbol] The name of the device
48
49
  # @return [Boolean] True if the device is registered, false otherwise
49
50
  def registered?(name)
50
- @registry.include?(name)
51
+ @lock.synchronize { @registry.include?(name) }
51
52
  end
52
53
 
53
54
  # Instantiate a new device with the specified options from the device registry.
@@ -58,7 +59,7 @@ module Lumberjack
58
59
  def new_device(name, options)
59
60
  klass = device_class(name)
60
61
  unless klass
61
- valid_names = @registry.keys.map(&:inspect).join(", ")
62
+ valid_names = registered_devices.keys.map(&:inspect).join(", ")
62
63
  raise ArgumentError.new("#{name.inspect} is not registered as a device name; valid names are: #{valid_names}")
63
64
  end
64
65
 
@@ -76,14 +77,14 @@ module Lumberjack
76
77
  # @param name [Symbol] The name of the device
77
78
  # @return [Class, nil] The registered device class or nil if not found
78
79
  def device_class(name)
79
- @registry[name]
80
+ @lock.synchronize { @registry[name] }
80
81
  end
81
82
 
82
83
  # Return the map of registered device class names.
83
84
  #
84
85
  # @return [Hash]
85
86
  def registered_devices
86
- @registry.dup
87
+ @lock.synchronize { @registry.dup }
87
88
  end
88
89
  end
89
90
  end
@@ -293,8 +293,10 @@ module Lumberjack
293
293
  message_attributes = Utils.flatten_attributes(message_attributes) if message_attributes
294
294
 
295
295
  attributes = merge_attributes(attributes, message_attributes) if message_attributes
296
- attributes = AttributesHelper.expand_runtime_values(attributes)
297
- attributes = attribute_formatter.format(attributes) if attributes && attribute_formatter
296
+ if attributes
297
+ attributes = AttributesHelper.expand_runtime_values(attributes)
298
+ attributes = attribute_formatter.format(attributes) if attribute_formatter
299
+ end
298
300
 
299
301
  [message, attributes]
300
302
  end
@@ -16,7 +16,7 @@ module Lumberjack
16
16
 
17
17
  # Exception raised when a circular reference is detected during traversal.
18
18
  # This prevents infinite recursion when formatting objects that reference themselves.
19
- class RecusiveReferenceError < StandardError
19
+ class RecursiveReferenceError < StandardError
20
20
  end
21
21
 
22
22
  # @param formatter [Formatter, nil] The formatter to call on each element
@@ -42,7 +42,7 @@ module Lumberjack
42
42
  hash = {}
43
43
  obj.each do |name, value|
44
44
  value = call_with_references(value, references)
45
- hash[name.to_s] = value unless value.is_a?(RecusiveReferenceError)
45
+ hash[name.to_s] = value unless value.is_a?(RecursiveReferenceError)
46
46
  end
47
47
  hash
48
48
  end
@@ -51,7 +51,7 @@ module Lumberjack
51
51
  array = []
52
52
  obj.each do |value|
53
53
  value = call_with_references(value, references)
54
- array << value unless value.is_a?(RecusiveReferenceError)
54
+ array << value unless value.is_a?(RecursiveReferenceError)
55
55
  end
56
56
  array
57
57
  end
@@ -64,7 +64,7 @@ module Lumberjack
64
64
 
65
65
  def with_object_reference(obj, references)
66
66
  if obj.is_a?(Enumerable)
67
- return RecusiveReferenceError.new if references.include?(obj.object_id)
67
+ return RecursiveReferenceError.new if references.include?(obj.object_id)
68
68
 
69
69
  references << obj.object_id
70
70
  begin
@@ -13,7 +13,7 @@ module Lumberjack
13
13
  def call(tags)
14
14
  tags = tags.collect { |key, value| "#{key}=#{value}" } if tags.is_a?(Hash)
15
15
  if tags.is_a?(Array)
16
- tags.collect { |tag| format_tag(tag) }.join(" ") unless tags.empty?
16
+ tags.collect { |tag| format_tag(tag) }.join(" ")
17
17
  else
18
18
  format_tag(tags)
19
19
  end
@@ -23,9 +23,9 @@ module Lumberjack
23
23
 
24
24
  def format_tag(tag)
25
25
  if tag.is_a?(Hash)
26
- tag.collect { |key, value| "[#{key}=#{value.strip}]" }.join(" ")
26
+ tag.collect { |key, value| "[#{key}=#{value.to_s.strip}]" }.join(" ")
27
27
  else
28
- "[#{tag.strip}]"
28
+ "[#{tag.to_s.strip}]"
29
29
  end
30
30
  end
31
31
  end
@@ -46,7 +46,6 @@ module Lumberjack
46
46
  require_relative "formatter/structured_formatter"
47
47
  require_relative "formatter/tags_formatter"
48
48
  require_relative "formatter/truncate_formatter"
49
- require_relative "formatter/tagged_message"
50
49
 
51
50
  class << self
52
51
  # Build a new formatter using a configuration block. The block receives the new formatter
@@ -67,16 +66,6 @@ module Lumberjack
67
66
  formatter
68
67
  end
69
68
 
70
- # Create a new empty formatter with no mappings. This is an alias for #new.
71
- #
72
- # @return [Lumberjack::Formatter] A new formatter with no default mappings.
73
- # @deprecated Use #new instead.
74
- def empty
75
- Utils.deprecated("Formatter.empty", "Lumberjack::Formatter.empty is deprecated and will be removed in version 2.1; use new instead.") do
76
- new
77
- end
78
- end
79
-
80
69
  # Create a new formatter with default mappings.
81
70
  #
82
71
  # Object: inspect formatter
@@ -18,6 +18,7 @@ module Lumberjack
18
18
  # end
19
19
  module FormatterRegistry
20
20
  @registry = {}
21
+ @lock = Mutex.new
21
22
 
22
23
  class << self
23
24
  # Register a formatter name. Formatter names can be used to associate a symbol with a formatter
@@ -37,7 +38,7 @@ module Lumberjack
37
38
  formatter ||= block
38
39
  raise ArgumentError.new("formatter must be a class or respond to call") unless formatter.is_a?(Class) || formatter.respond_to?(:call)
39
40
 
40
- @registry[name] = formatter
41
+ @lock.synchronize { @registry[name] = formatter }
41
42
  end
42
43
 
43
44
  # Remove a formatter from the registry.
@@ -45,7 +46,7 @@ module Lumberjack
45
46
  # @param name [Symbol] The name of the formatter to remove
46
47
  # @return [void]
47
48
  def remove(name)
48
- @registry.delete(name)
49
+ @lock.synchronize { @registry.delete(name) }
49
50
  end
50
51
 
51
52
  # Check if a formatter is registered.
@@ -53,7 +54,7 @@ module Lumberjack
53
54
  # @param name [Symbol] The name of the formatter
54
55
  # @return [Boolean] True if the formatter is registered, false otherwise
55
56
  def registered?(name)
56
- @registry.include?(name)
57
+ @lock.synchronize { @registry.include?(name) }
57
58
  end
58
59
 
59
60
  # Retrieve the formatter registered with the given name or nil if the name is not defined.
@@ -61,10 +62,10 @@ module Lumberjack
61
62
  # @param name [Symbol] The name of the formatter
62
63
  # @return [#call, nil] The registered formatter class or nil if not found
63
64
  def formatter(name, *args)
64
- instance = @registry[name]
65
+ instance = @lock.synchronize { @registry[name] }
65
66
 
66
67
  if instance.nil?
67
- valid_names = @registry.keys.map(&:inspect).join(", ")
68
+ valid_names = registered_formatters.keys.map(&:inspect).join(", ")
68
69
  raise ArgumentError.new("#{name.inspect} is not registered as a formatter name; valid names are: #{valid_names}")
69
70
  end
70
71
 
@@ -77,7 +78,7 @@ module Lumberjack
77
78
  #
78
79
  # @return [Hash]
79
80
  def registered_formatters
80
- @registry.dup
81
+ @lock.synchronize { @registry.dup }
81
82
  end
82
83
  end
83
84
  end
@@ -94,17 +94,6 @@ module Lumberjack
94
94
  attributes == other.attributes
95
95
  end
96
96
 
97
- # Alias for tags to provide backward compatibility with version 1.x API. This method
98
- # will eventually be removed.
99
- #
100
- # @return [Hash, nil] The attributes of the log entry.
101
- # @deprecated Use {#attributes} instead.
102
- def tags
103
- Utils.deprecated("LogEntry#tags", "Lumberjack::LogEntry#tags is deprecated and will be removed in version 2.1; use attributes instead.") do
104
- attributes
105
- end
106
- end
107
-
108
97
  # Access an attribute value by name. Supports both simple and nested attribute
109
98
  # access using dot notation for hierarchical data structures.
110
99
  #
@@ -116,17 +105,6 @@ module Lumberjack
116
105
  AttributesHelper.new(attributes)[name]
117
106
  end
118
107
 
119
- # Alias method for #[] to provide backward compatibility with version 1.x API. This
120
- # method will eventually be removed.
121
- #
122
- # @return [Hash]
123
- # @deprecated Use {#[]} instead.
124
- def tag(name)
125
- Utils.deprecated("LogEntry#tag", "Lumberjack::LogEntry#tag is deprecated and will be removed in version 2.1; use [] instead.") do
126
- self[name]
127
- end
128
- end
129
-
130
108
  # Expand flat attributes with dot notation into a nested hash structure.
131
109
  # Attributes containing dots in their names are converted into hierarchical
132
110
  # nested hashes for structured data representation.
@@ -136,17 +114,6 @@ module Lumberjack
136
114
  Utils.expand_attributes(attributes)
137
115
  end
138
116
 
139
- # Alias for nested_attributes to provide API compatibility with version 1.x.
140
- # This method will eventually be removed.
141
- #
142
- # @return [Hash]
143
- # @deprecated Use {#nested_attributes} instead.
144
- def nested_tags
145
- Utils.deprecated("LogEntry#nested_tags", "Lumberjack::LogEntry#nested_tags is deprecated and will be removed in version 2.1; use nested_attributes instead.") do
146
- nested_attributes
147
- end
148
- end
149
-
150
117
  # Determine if the log entry contains no meaningful content. An entry is
151
118
  # considered empty if it has no message content and no attributes.
152
119
  #
@@ -183,30 +150,30 @@ module Lumberjack
183
150
  attributes_string
184
151
  end
185
152
 
186
- # Flatten nested attributes and remove empty values.
153
+ # Flatten nested attributes and remove empty values. The returned hash is always
154
+ # a new copy so that the entry does not alias a hash owned by the caller (such as
155
+ # a live logger context hash that could be mutated after the entry is created).
187
156
  #
188
157
  # @param attributes [Hash] The attributes hash to compact
189
158
  # @return [Hash] The flattened attributes with empty values removed
190
159
  def flatten_attributes(attributes)
191
- unless attributes.all? { |key, value| key.is_a?(String) && !value.is_a?(Hash) }
192
- attributes = Utils.flatten_attributes(attributes)
193
- end
194
-
160
+ needs_flattening = false
195
161
  delete_keys = nil
196
162
  attributes.each do |key, value|
197
- if value.nil? || value == ""
198
- delete_keys ||= []
199
- delete_keys << key
200
- elsif value.is_a?(Array) && value.empty?
163
+ needs_flattening = true if !key.is_a?(String) || value.is_a?(Hash)
164
+ if value.nil? || value == "" || (value.is_a?(Array) && value.empty?)
201
165
  delete_keys ||= []
202
166
  delete_keys << key
203
167
  end
204
168
  end
205
169
 
206
- return attributes if delete_keys.nil?
207
-
208
- attributes = attributes.dup
209
- delete_keys&.each { |key| attributes.delete(key) }
170
+ if needs_flattening
171
+ attributes = Utils.flatten_attributes(attributes)
172
+ attributes.delete_if { |_key, value| value.nil? || value == "" || (value.is_a?(Array) && value.empty?) }
173
+ else
174
+ attributes = attributes.dup
175
+ delete_keys&.each { |key| attributes.delete(key) }
176
+ end
210
177
 
211
178
  attributes
212
179
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A minimal hash implementation that allows values to be looked up with either
4
+ # string or symbol keys.
5
+ #
6
+ # The keys stored in the hash are left exactly as they are; only lookups are
7
+ # indifferent. Log entry attributes are matched with string keys, but matchers
8
+ # that do their own key lookups on the attributes hash (i.e. RSpec's
9
+ # `hash_including`) have no way of knowing that. Wrapping the attributes in this
10
+ # class lets those matchers use either form.
11
+ #
12
+ # @api private
13
+ class Lumberjack::LogEntryMatcher::IndifferentHash < Hash
14
+ class << self
15
+ # Recursively wrap a value so that any hashes in it, including hashes nested
16
+ # inside arrays, allow indifferent key lookups. Values that are not hashes or
17
+ # arrays are returned as is.
18
+ #
19
+ # @param value [Object] The value to wrap.
20
+ # @return [Object] The wrapped value.
21
+ def wrap(value)
22
+ case value
23
+ when self
24
+ value
25
+ when Hash
26
+ value.each_with_object(new) { |(key, val), hash| hash[key] = wrap(val) }
27
+ when Array
28
+ value.collect { |val| wrap(val) }
29
+ else
30
+ value
31
+ end
32
+ end
33
+ end
34
+
35
+ # Capture the unaliased implementation so the overrides below can check for a
36
+ # key without recursing back into themselves.
37
+ alias_method :stored_key?, :key?
38
+ private :stored_key?
39
+
40
+ def [](key)
41
+ super(resolve_key(key))
42
+ end
43
+
44
+ def fetch(key, *args, &block)
45
+ super(resolve_key(key), *args, &block)
46
+ end
47
+
48
+ def dig(key, *rest)
49
+ super(resolve_key(key), *rest)
50
+ end
51
+
52
+ def values_at(*keys)
53
+ super(*keys.collect { |key| resolve_key(key) })
54
+ end
55
+
56
+ def key?(key)
57
+ stored_key?(resolve_key(key))
58
+ end
59
+
60
+ alias_method :has_key?, :key?
61
+ alias_method :include?, :key?
62
+ alias_method :member?, :key?
63
+
64
+ private
65
+
66
+ # Return the key as it is stored in the hash. If the key is not present in
67
+ # either its string or symbol form, then the key itself is returned so that
68
+ # normal missing key semantics apply.
69
+ #
70
+ # @param key [Object] The key being looked up.
71
+ # @return [Object] The key to use for the lookup.
72
+ def resolve_key(key)
73
+ return key if stored_key?(key)
74
+
75
+ alternate = if key.is_a?(String)
76
+ key.to_sym
77
+ elsif key.is_a?(Symbol)
78
+ key.to_s
79
+ end
80
+
81
+ (alternate && stored_key?(alternate)) ? alternate : key
82
+ end
83
+ end
@@ -47,9 +47,15 @@ class Lumberjack::LogEntryMatcher::Score
47
47
  end
48
48
 
49
49
  # Check attributes match
50
- if attributes.is_a?(Hash) && !attributes.empty?
51
- attributes_score = calculate_attributes_score(entry.attributes, attributes)
52
- scores << attributes_score
50
+ if attributes.is_a?(Hash)
51
+ unless attributes.empty?
52
+ scores << calculate_attributes_score(entry.attributes, attributes)
53
+ weights << 0.3
54
+ end
55
+ elsif attributes
56
+ # Matchers like RSpec's hash_including are applied to the attributes hash as a whole.
57
+ entry_attributes = Lumberjack::LogEntryMatcher::IndifferentHash.wrap(Lumberjack::Utils.expand_attributes(entry.attributes))
58
+ scores << calculate_field_score(entry_attributes, attributes)
53
59
  weights << 0.3
54
60
  end
55
61
 
@@ -132,7 +138,9 @@ class Lumberjack::LogEntryMatcher::Score
132
138
  return 0.0 unless entry_attributes && attributes_filter.is_a?(Hash)
133
139
 
134
140
  attributes_filter = deep_stringify_keys(Lumberjack::Utils.expand_attributes(attributes_filter))
135
- attributes = deep_stringify_keys(Lumberjack::Utils.expand_attributes(entry_attributes))
141
+ attributes = Lumberjack::LogEntryMatcher::IndifferentHash.wrap(
142
+ deep_stringify_keys(Lumberjack::Utils.expand_attributes(entry_attributes))
143
+ )
136
144
 
137
145
  total_attribute_filters = count_attribute_filters(attributes_filter)
138
146
  return 0.0 if total_attribute_filters == 0