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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e1af0567acc694ebd29f04587e9203db03d14958306abd6a691721c918cd024
4
- data.tar.gz: 0eab429fc4ef909b57cec680fc84ab5cb12fd99a0e986d105d497be9dee97369
3
+ metadata.gz: e2209d8f5b5a64c28765eb9e0756c2723354430017e8fc718ed1238678534f62
4
+ data.tar.gz: 3f7d07d946b9975962e9c25186cffab9a2529142430ecd1a833ba7a3bcb4dc9f
5
5
  SHA512:
6
- metadata.gz: 3894fec45c2f423348c6926c5bef9c1aff794e5e3d7e11394bd791be74d606ad0ad4e5613c7295b9b4051af3a6f9089a6ac60ade8f86a7a407f4d346fcb80df7
7
- data.tar.gz: 608590677da050618459e44be516f72b00d9a8e8db7edc2be63fc7622864936db74b429ce013b2e12c75da9d65e5b89b289b49cecf2683803353a2832f45463a
6
+ metadata.gz: 7a2c5fc583af252f983e936ed54fe75a40a3f36eec349f6ba219f798129d86022d33c4a65e3aca72d75f7c1a8a14900f94d2cea6ab74d03f96607306a5b0fde9
7
+ data.tar.gz: d8c7de0eb56182e0ca30c76a477281cd13fd7c728e4f837e0adc578ade6056177fcd4375c67a38e1bd85e788520f14a9c2bde6315313f368e53413eb622aae21
data/ARCHITECTURE.md CHANGED
@@ -148,21 +148,6 @@ classDiagram
148
148
  +reopen(logdev) void
149
149
  }
150
150
 
151
- class DeviceDateRollingLogFile["Device::DateRollingLogFile"] {
152
- +String path
153
- +String frequency
154
- +initialize(path, options)
155
- +roll_file?() Boolean
156
- }
157
-
158
- class DeviceSizeRollingLogFile["Device::SizeRollingLogFile"] {
159
- +String path
160
- +Integer max_size
161
- +Integer keep
162
- +initialize(path, options)
163
- +roll_file?() Boolean
164
- }
165
-
166
151
  class DeviceMulti["Device::Multi"] {
167
152
  +Array devices
168
153
  +initialize(*devices)
@@ -251,8 +236,6 @@ classDiagram
251
236
  Device <|-- DeviceNull : implements
252
237
  Device <|-- DeviceLoggerWrapper : implements
253
238
  DeviceWriter <|-- DeviceLogFile : inherits
254
- DeviceLogFile <|-- DeviceDateRollingLogFile : inherits
255
- DeviceLogFile <|-- DeviceSizeRollingLogFile : inherits
256
239
 
257
240
  DeviceWriter --* Template : uses
258
241
  DeviceWriter --* DeviceBuffer : uses
data/CHANGELOG.md CHANGED
@@ -4,6 +4,58 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 2.1.0
8
+
9
+ ### Added
10
+
11
+ - Added `Lumberjack::LogEntryMatcher#diff` which returns only the fields that do not match a log entry, with the expected and actual values. This can be used to diagnose why a test assertion failed.
12
+ - Added a `formatter` option to `Lumberjack::LogEntryMatcher`. Filter values are compared raw first; if that fails, they are formatted with the entry formatter and compared again. This allows expectations to be written with unformatted values (an Exception, for example) and still match the formatted values captured on a log entry. `#diff` reports expected values in their formatted form so both sides of a mismatch are in the same form.
13
+ - Added `Lumberjack::Device::Test#entry_formatter` (also settable with the `:entry_formatter` option) which is used as the default formatter for `include?`, `match`, and `closest_match`. Those methods also accept a per call `formatter` argument.
14
+
15
+ ### Fixed
16
+
17
+ - `Lumberjack::Device::Multi#reopen` now passes the log destination through to the wrapped devices instead of always passing `nil`.
18
+ - `Lumberjack::Device::Multi#datetime_format` no longer raises an error when none of the wrapped devices have a datetime format.
19
+ - `Lumberjack::TemplateRegistry.registered?` no longer raises a `NoMethodError`.
20
+ - `Lumberjack::Formatter::TagsFormatter` no longer raises an error on non-string tag values and now returns an empty string instead of `nil` for empty tag arrays.
21
+ - The `before_flush` callback on `Lumberjack::Device::Buffer` is now invoked outside of the buffer lock so callbacks that log or flush no longer deadlock. As a result, the callback may be invoked concurrently by multiple threads and must be thread safe if it modifies shared state.
22
+ - Hardened `Lumberjack::Device::Buffer` against race conditions that could drop entries when writing concurrently with `flush` or `close`. Concurrent flushes are now serialized so buffered batches can no longer be interleaved or reordered when written to the wrapped device.
23
+ - `Lumberjack::Device::Test#clear` and `#last_entry` are now thread safe.
24
+ - Device, formatter, and template registries are now thread safe.
25
+ - Log entries no longer share their attributes hash with the caller, so mutating a logging context after an entry was created no longer changes entries held in memory by buffered or test devices.
26
+ - Contexts no longer share array attribute values with their parent context, so appending to an inherited array attribute no longer mutates the parent context.
27
+ - `Lumberjack.context` called without a block now returns the current context as documented instead of raising an `ArgumentError`.
28
+ - Renamed misspelled `Lumberjack::Formatter::StructuredFormatter::RecusiveReferenceError` to `RecursiveReferenceError`. The old constant is kept as an alias for backward compatibility.
29
+ - `Lumberjack::LogEntryMatcher` no longer ignores an `attributes` filter that is not a hash. Passing a matcher like RSpec's `hash_including` as the entire filter silently matched every entry; it is now applied to the entry's attributes hash with `===`.
30
+ - `Lumberjack::LogEntryMatcher` now exposes log entry attributes to matchers with indifferent key access, so matchers that do their own key lookups (i.e. RSpec's `hash_including`) can use either string or symbol attribute names.
31
+
32
+ ### Removed
33
+
34
+ - Removed the methods, arguments, and classes that were deprecated in version 2.0.
35
+ - `Lumberjack.context?` (use `Lumberjack.in_context?`)
36
+ - `Lumberjack.context_tags` (use `Lumberjack.context_attributes`)
37
+ - `Lumberjack::Logger#tags` (use `#attributes`)
38
+ - `Lumberjack::Logger#tag_value` (use `#attribute_value`)
39
+ - `Lumberjack::Logger#tag_globally` (use `#tag!`)
40
+ - `Lumberjack::Logger#in_tag_context?` (use `#in_context?`)
41
+ - `Lumberjack::Logger#remove_tag` (use `#untag` or `#untag!`)
42
+ - `Lumberjack::Logger#tag_formatter` and `#tag_formatter=` (use `#attribute_formatter` and `#attribute_formatter=`)
43
+ - `Lumberjack::Logger#set_progname` (use `#with_progname` or `#progname=`)
44
+ - `Lumberjack::Logger#tagged`, `#untagged`, `#log_at`, and `#silence` (install the `lumberjack_rails` gem or use `#append_to`, `#clear_attributes`, and `#with_level`)
45
+ - Passing an options hash as the second argument to `Lumberjack::Logger.new` (use keyword arguments)
46
+ - The `:roll`, `:max_size`, `:message_formatter`, and `:tag_formatter` options on `Lumberjack::Logger.new` (use the `shift_age`, `shift_size`, and `formatter` arguments)
47
+ - `Lumberjack::LogEntry#tags`, `#tag`, and `#nested_tags` (use `#attributes`, `#[]`, and `#nested_attributes`)
48
+ - `Lumberjack::Formatter.empty` (use `.new`)
49
+ - `Lumberjack::AttributeFormatter#add` and `#remove` (use `#add_class`/`#add_attribute` and `#remove_class`/`#remove_attribute`)
50
+ - `Lumberjack::Utils.flatten_tags` and `.expand_tags` (use `.flatten_attributes` and `.expand_attributes`)
51
+ - `Lumberjack::TagFormatter` (use `Lumberjack::AttributeFormatter`)
52
+ - `Lumberjack::TagContext` (use `Lumberjack::AttributesHelper`)
53
+ - `Lumberjack::Tags` (use `Lumberjack::AttributesHelper`)
54
+ - `Lumberjack::Formatter::TaggedMessage` (use `Lumberjack::MessageAttributes`)
55
+ - `Lumberjack::Device::DateRollingLogFile` and `Lumberjack::Device::SizeRollingLogFile` (use `Lumberjack::Device::LogFile`)
56
+ - `Lumberjack::Formatter::StructuredFormatter::RecusiveReferenceError` misspelled alias (use `RecursiveReferenceError`)
57
+ - Version 1.x template syntax using `:placeholder` (use `{{placeholder}}`)
58
+
7
59
  ## 2.0.5
8
60
 
9
61
  ### Added
data/README.md CHANGED
@@ -724,6 +724,41 @@ You should make sure to call `logger.device.clear` between tests to clear the ca
724
724
  > [!NOTE]
725
725
  > Log entries are captured after formatters have been applied. This provides a mechanism for including the formatting logic in your tests.
726
726
 
727
+ Since entries are captured after formatting, you can set an entry formatter on the `Test` device to write expectations with unformatted values. Filter values are compared raw first; if that fails, they are formatted and compared again.
728
+
729
+ ```ruby
730
+ formatter = Lumberjack::EntryFormatter.build do |config|
731
+ config.format_message(Exception) do |e|
732
+ Lumberjack::MessageAttributes.new(e.inspect, {error: {kind: e.class.name, message: e.message, trace: e.backtrace}})
733
+ end
734
+ config.format_attributes(Exception) do |e|
735
+ {kind: e.class.name, message: e.message, trace: e.backtrace}
736
+ end
737
+ end
738
+
739
+ logger = Lumberjack::Logger.new(:test, formatter: formatter)
740
+ logger.device.entry_formatter = logger.formatter
741
+
742
+ exception = RuntimeError.new("boom")
743
+ logger.error(exception)
744
+
745
+ # These all match the same entry.
746
+ expect(logger.device).to include(message: exception)
747
+ expect(logger.device).to include(message: exception.inspect, attributes: {error: exception})
748
+ expect(logger.device).to include(
749
+ message: exception.inspect,
750
+ attributes: {error: {kind: "RuntimeError", message: "boom"}}
751
+ )
752
+ ```
753
+
754
+ You can use `Lumberjack::LogEntryMatcher#diff` to see why an entry did not match. It returns only the fields that do not match, with the expected and actual values.
755
+
756
+ ```ruby
757
+ matcher = Lumberjack::LogEntryMatcher.new(message: "User logged in", attributes: {user_id: 456})
758
+ matcher.diff(logger.device.last_entry)
759
+ # => {"attributes" => {"user_id" => {expected: 456, actual: 123}}}
760
+ ```
761
+
727
762
  > [!TIP]
728
763
  > The [lumberjack_capture_device](https://github.com/bdurand/lumberjack_capture_device) gem provides some additional testing utilities and rspec integration.
729
764
 
data/UPGRADE_GUIDE.md CHANGED
@@ -45,7 +45,7 @@ logger.tag(attributes) do
45
45
  end
46
46
  ```
47
47
 
48
- Internal uses of the word "tag" have all been updated to use "attribute" instead. The "tag" versions of the methods will still work, but they have been [marked as deprecated](CHANGELOG.md#deprecated) and will be removed in a future version.
48
+ Internal uses of the word "tag" have all been updated to use "attribute" instead. The "tag" versions of the methods still work in version 2.0, but they are deprecated and have been removed in version 2.1.
49
49
 
50
50
  Global attributes are now set with the `tag!` method instead of `tag_globally` or calling `tag` outside of a context.
51
51
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.5
1
+ 2.1.0
@@ -95,37 +95,6 @@ module Lumberjack
95
95
  self
96
96
  end
97
97
 
98
- # Add formatters for specific attribute names or object classes. This is a convenience method
99
- # that automatically delegates to {#add_class} or {#add_attribute} based on the input type.
100
- #
101
- # When you pass a Module/Class, it creates a class-based formatter that applies to all
102
- # attribute values of that type. When you pass a String, it creates an attribute-specific
103
- # formatter for that exact attribute name.
104
- #
105
- # Class formatters are applied recursively to nested hashes and arrays, making them
106
- # powerful for formatting complex nested structures.
107
- #
108
- # @param names_or_classes [String, Module, Array<String, Module>] Attribute names or object classes.
109
- # @param formatter [Lumberjack::Formatter, #call, Symbol, nil] The formatter to use.
110
- # @yield [value] Block-based formatter that receives the attribute value.
111
- # @yieldparam value [Object] The attribute value to format.
112
- # @yieldreturn [Object] The formatted attribute value.
113
- # @return [Lumberjack::AttributeFormatter] Returns self for method chaining.
114
- # @deprecated Use {#add_class} or {#add_attribute} instead.
115
- def add(names_or_classes, formatter = nil, *args, &block)
116
- Utils.deprecated("AttributeFormatter#add", "AttributeFormatter#add is deprecated and will be removed in version 2.1; use #add_class or #add_attribute instead.") do
117
- Array(names_or_classes).each do |obj|
118
- if obj.is_a?(Module)
119
- add_class(obj, formatter, *args, &block)
120
- else
121
- add_attribute(obj, formatter, *args, &block)
122
- end
123
- end
124
- end
125
-
126
- self
127
- end
128
-
129
98
  # Add formatters for specific object classes. The formatter will be applied to any attribute
130
99
  # value that is an instance of the registered class. This is particularly useful for formatting
131
100
  # all instances of specific data types consistently across your logs.
@@ -203,26 +172,6 @@ module Lumberjack
203
172
  self
204
173
  end
205
174
 
206
- # Remove formatters for specific attribute names or classes. This reverts the specified
207
- # attributes or classes to use the default formatter (if configured) or no formatting.
208
- #
209
- # @param names_or_classes [String, Module, Array<String, Module>] Attribute names or classes
210
- # to remove formatters for.
211
- # @return [Lumberjack::AttributeFormatter] Returns self for method chaining.
212
- # @deprecated Use {#remove_class} or {#remove_attribute} instead.
213
- def remove(names_or_classes)
214
- Utils.deprecated("AttributeFormatter#remove", "AttributeFormatter#remove is deprecated and will be removed in version 2.1; use #remove_class or #remove_attribute instead.") do
215
- Array(names_or_classes).each do |key|
216
- if key.is_a?(Module)
217
- @class_formatter.remove(key)
218
- else
219
- @attribute_formatter.delete(key.to_s)
220
- end
221
- end
222
- end
223
- self
224
- end
225
-
226
175
  # Remove formatters for specific object classes. This reverts the specified classes
227
176
  # to use the default formatter (if configured) or no formatting.
228
177
  #
@@ -46,7 +46,12 @@ module Lumberjack
46
46
  @default_severity = nil
47
47
 
48
48
  if parent_context
49
- @attributes = parent_context.attributes.dup if parent_context.attributes
49
+ if parent_context.attributes
50
+ # Dup array values so appending tags in this context does not mutate the parent.
51
+ @attributes = parent_context.attributes.transform_values do |value|
52
+ value.is_a?(Array) ? value.dup : value
53
+ end
54
+ end
50
55
  self.level = parent_context.level
51
56
  self.progname = parent_context.progname
52
57
  end
@@ -7,11 +7,10 @@ module Lumberjack
7
7
  #
8
8
  # @api private
9
9
  class Data
10
- attr_accessor :context, :logging, :cleared
10
+ attr_accessor :context, :cleared
11
11
 
12
12
  def initialize(copy = nil)
13
13
  @context = copy&.context
14
- @logging = copy&.logging
15
14
  @cleared = copy&.cleared
16
15
  end
17
16
  end
@@ -73,6 +72,15 @@ module Lumberjack
73
72
  def current_context_locals
74
73
  return nil unless defined?(@context_locals)
75
74
 
75
+ # Fast path that avoids the mutex when no context locals are set anywhere. This is
76
+ # safe without synchronization: Hash#empty? only reads the size and cannot raise or
77
+ # return garbage during a concurrent update, so the worst case is a stale value. A
78
+ # stale false falls through to the synchronized lookup, which is always correct. A
79
+ # stale true cannot hide an entry for the current scope because entries for the
80
+ # current scope are only ever written from code running in that same scope, so any
81
+ # such write is already visible here by program order.
82
+ return nil if @context_locals.empty?
83
+
76
84
  scope_id = context_locals_scope_id
77
85
  @context_locals_mutex.synchronize do
78
86
  @context_locals[scope_id]
@@ -549,7 +549,8 @@ module Lumberjack
549
549
  # Dereference arguments to log calls so we can have methods with compatibility with ::Logger
550
550
  def call_add_entry(severity, message_or_progname_or_attributes, progname_or_attributes, &block) # :nodoc:
551
551
  severity = Severity.coerce(severity) unless severity.is_a?(Integer)
552
- return true unless level.nil? || severity >= level
552
+ current_level = level
553
+ return true unless current_level.nil? || severity >= current_level
553
554
 
554
555
  message = nil
555
556
  progname = nil
@@ -592,10 +593,13 @@ module Lumberjack
592
593
  end
593
594
  end
594
595
 
595
- def merge_all_attributes
596
+ # Merge the attributes from the global context, default context, current context,
597
+ # and the optional extra attributes into a single hash. The context locals can be
598
+ # passed in to avoid looking them up again if the caller has already done so.
599
+ def merge_all_attributes(locals = current_context_locals, extra_attributes = nil)
596
600
  attributes = nil
597
601
 
598
- unless current_context_locals&.cleared
602
+ unless locals&.cleared
599
603
  global_context_attributes = Lumberjack.context_attributes
600
604
  if global_context_attributes && !global_context_attributes.empty?
601
605
  attributes ||= {}
@@ -609,12 +613,18 @@ module Lumberjack
609
613
  end
610
614
  end
611
615
 
612
- context_attributes = current_context&.attributes
616
+ context_attributes = (locals&.context || default_context)&.attributes
613
617
  if context_attributes && !context_attributes.empty?
614
618
  attributes ||= {}
615
619
  attributes.merge!(context_attributes)
616
620
  end
617
621
 
622
+ if extra_attributes
623
+ # The merged hash is always freshly allocated, so it is safe to merge into it
624
+ # without exposing the mutation to any caller-visible hash.
625
+ attributes = attributes ? attributes.merge!(extra_attributes) : extra_attributes
626
+ end
627
+
618
628
  attributes
619
629
  end
620
630
  end
@@ -18,69 +18,93 @@ module Lumberjack
18
18
  class EntryBuffer
19
19
  attr_accessor :size
20
20
 
21
- attr_reader :device, :last_flushed_at
21
+ attr_reader :device
22
22
 
23
23
  def initialize(device, size, before_flush)
24
24
  @device = device
25
25
  @size = size
26
26
  @before_flush = before_flush if before_flush.respond_to?(:call)
27
+ @before_flush_guard = :"lumberjack_device_buffer_before_flush_#{object_id}"
27
28
  @lock = Mutex.new
29
+ @flush_lock = Mutex.new
28
30
  @entries = []
29
31
  @last_flushed_at = Time.now
30
32
  @closed = false
31
33
  end
32
34
 
33
35
  def <<(entry)
34
- return if closed?
36
+ flush_needed = false
35
37
 
36
38
  @lock.synchronize do
37
- @entries << entry
39
+ unless @closed
40
+ @entries << entry
41
+ flush_needed = @entries.size >= @size
42
+ end
38
43
  end
39
44
 
40
- flush if @entries.size >= @size
45
+ flush if flush_needed
41
46
  end
42
47
 
48
+ # Concurrent flushes are serialized by a separate lock so that batches cannot be
49
+ # interleaved or reordered when written to the wrapped device. The entry lock is
50
+ # only held while swapping out the buffered entries so that threads writing new
51
+ # entries are not blocked while the wrapped device performs I/O.
43
52
  def flush
44
- entries = nil
53
+ call_before_flush
45
54
 
46
- if closed?
47
- @before_flush&.call
48
- entries = @entries
49
- @entries = []
50
- else
55
+ @flush_lock.synchronize do
56
+ entries = nil
51
57
  @lock.synchronize do
52
- @before_flush&.call
53
58
  entries = @entries
54
59
  @entries = []
60
+ @last_flushed_at = Time.now
55
61
  end
56
- end
57
-
58
- @last_flushed_at = Time.now
59
-
60
- return if entries.nil?
61
62
 
62
- entries.each do |entry|
63
- @device.write(entry)
64
- rescue => e
65
- warn("Error writing log entry from buffer: #{e.inspect}")
63
+ entries.each do |entry|
64
+ @device.write(entry)
65
+ rescue => e
66
+ warn("Error writing log entry from buffer: #{e.inspect}")
67
+ end
66
68
  end
67
69
  end
68
70
 
69
71
  def close
70
- @closed = true
72
+ @lock.synchronize { @closed = true }
71
73
  flush
72
74
  end
73
75
 
74
76
  def closed?
75
- @closed
77
+ @lock.synchronize { @closed }
76
78
  end
77
79
 
78
80
  def reopen
79
- @closed = false
81
+ @lock.synchronize { @closed = false }
80
82
  end
81
83
 
82
84
  def empty?
83
- @entries.empty?
85
+ @lock.synchronize { @entries.empty? }
86
+ end
87
+
88
+ def last_flushed_at
89
+ @lock.synchronize { @last_flushed_at }
90
+ end
91
+
92
+ private
93
+
94
+ # The callback must be invoked outside the mutex so that a callback that logs
95
+ # through this buffer cannot deadlock on a recursive lock. The thread local
96
+ # guards against infinite recursion when a callback write triggers another flush.
97
+ # The guard is scoped per instance so a callback that logs through a different
98
+ # buffer does not suppress that buffer's own callback.
99
+ def call_before_flush
100
+ return if @before_flush.nil? || Thread.current[@before_flush_guard]
101
+
102
+ begin
103
+ Thread.current[@before_flush_guard] = true
104
+ @before_flush.call
105
+ ensure
106
+ Thread.current[@before_flush_guard] = nil
107
+ end
84
108
  end
85
109
  end
86
110
 
@@ -110,7 +134,9 @@ module Lumberjack
110
134
  # @option options [Integer] :buffer_size The number of entries to buffer before flushing. Default is 0 (no buffering).
111
135
  # @option options [Integer] :flush_seconds If specified, a background thread will flush the buffer every N seconds.
112
136
  # @option options [Proc] :before_flush A callback that will be called before each flush. The callback should
113
- # respond to +call+ and take no arguments.
137
+ # respond to +call+ and take no arguments. The callback is invoked outside of the buffer lock, so it may
138
+ # be called concurrently from multiple threads flushing at the same time; it must be thread safe if it
139
+ # modifies any shared state.
114
140
  def initialize(wrapped_device, options = {})
115
141
  buffer_options = [:buffer_size, :flush_seconds, :before_flush]
116
142
  device_options = options.reject { |k, _| buffer_options.include?(k) }
@@ -132,7 +158,7 @@ module Lumberjack
132
158
  # Set the buffer size. The underlying device will only be written to when the buffer size
133
159
  # is exceeded.
134
160
  #
135
- # @param [Integer] value The size of the buffer in bytes.
161
+ # @param [Integer] value The number of entries to buffer before flushing.
136
162
  # @return [void]
137
163
  def buffer_size=(value)
138
164
  @buffer.size = value
@@ -194,16 +220,5 @@ module Lumberjack
194
220
  def empty?
195
221
  @buffer.empty?
196
222
  end
197
-
198
- private
199
-
200
- def create_flusher_thread(flush_seconds, buffer) # :nodoc:
201
- Thread.new do
202
- until buffer.closed?
203
- sleep(flush_seconds)
204
- buffer.flush if Time.now - buffer.last_flushed_at >= flush_seconds
205
- end
206
- end
207
- end
208
223
  end
209
224
  end
@@ -80,7 +80,7 @@ module Lumberjack
80
80
  # @return [void]
81
81
  def reopen(logdev = nil)
82
82
  devices.each do |device|
83
- device.reopen(logdev = nil)
83
+ device.reopen(logdev)
84
84
  end
85
85
  end
86
86
 
@@ -91,7 +91,7 @@ module Lumberjack
91
91
  # @return [String, nil] The datetime format string from the first device
92
92
  # that has one configured, or nil if no devices have a format set
93
93
  def datetime_format
94
- devices.detect(&:datetime_format).datetime_format
94
+ devices.detect(&:datetime_format)&.datetime_format
95
95
  end
96
96
 
97
97
  # Set the datetime format on all configured devices that support it.
@@ -86,6 +86,17 @@ module Lumberjack
86
86
  # @return [Hash] A copy of the options hash passed during initialization
87
87
  attr_reader :options
88
88
 
89
+ # @!attribute [rw] entry_formatter
90
+ # Entry formatter used by +include?+, +match+, and +closest_match+ when
91
+ # building matchers. Log entries are captured after the logger's formatter
92
+ # has been applied, so setting this to the same formatter used by the logger
93
+ # allows expectations to be written with unformatted values:
94
+ #
95
+ # logger.device.entry_formatter = logger.formatter
96
+ #
97
+ # @return [Lumberjack::EntryFormatter, nil] The entry formatter used for matching
98
+ attr_accessor :entry_formatter
99
+
89
100
  class << self
90
101
  # Format a log entry or expectation hash into a more human readable format. This is
91
102
  # intended for use in test failure messages to help diagnose why a match failed when
@@ -116,17 +127,39 @@ module Lumberjack
116
127
  message << "#{indent_str}severity: #{Lumberjack::Severity.level_to_label(severity)}" if severity
117
128
  message << "#{indent_str}message: #{expectation["message"]}" if expectation.include?("message")
118
129
  message << "#{indent_str}progname: #{expectation["progname"]}" if expectation.include?("progname")
119
- if expectation["attributes"].is_a?(Hash) && !expectation["attributes"].empty?
120
- attributes = Lumberjack::Utils.flatten_attributes(expectation["attributes"])
121
- label = "attributes:"
122
- prefix = "#{indent_str}#{label}"
123
- attributes.sort_by(&:first).each do |name, value|
124
- message << "#{prefix} #{name}: #{value.inspect}"
125
- prefix = "#{indent_str}#{" " * label.length}"
130
+ expected_attributes = expectation["attributes"]
131
+ if expected_attributes.is_a?(Hash)
132
+ unless expected_attributes.empty?
133
+ attributes = Lumberjack::Utils.flatten_attributes(expected_attributes)
134
+ label = "attributes:"
135
+ prefix = "#{indent_str}#{label}"
136
+ attributes.sort_by(&:first).each do |name, value|
137
+ message << "#{prefix} #{name}: #{formatted_value(value)}"
138
+ prefix = "#{indent_str}#{" " * label.length}"
139
+ end
126
140
  end
141
+ elsif expected_attributes
142
+ # Matchers like RSpec's hash_including are matched against the attributes hash as a whole.
143
+ message << "#{indent_str}attributes: #{formatted_value(expected_attributes)}"
127
144
  end
128
145
  message.join(Lumberjack::LINE_SEPARATOR)
129
146
  end
147
+
148
+ private
149
+
150
+ # Format a value for display in an expectation. Matcher objects (i.e. RSpec matchers)
151
+ # that implement a +description+ method are displayed using that description since
152
+ # inspecting them is not very informative.
153
+ #
154
+ # @param value [Object] The value to format.
155
+ # @return [String] The formatted value.
156
+ def formatted_value(value)
157
+ if value.respond_to?(:description) && !value.is_a?(Module)
158
+ value.description.to_s
159
+ else
160
+ value.inspect
161
+ end
162
+ end
130
163
  end
131
164
 
132
165
  # Initialize a new Test device with configurable buffer management.
@@ -137,9 +170,12 @@ module Lumberjack
137
170
  # @option options [Integer] :max_entries (1000) The maximum number of entries
138
171
  # to retain in the buffer. When this limit is exceeded, the oldest entries
139
172
  # are automatically removed to maintain the size limit.
173
+ # @option options [Lumberjack::EntryFormatter] :entry_formatter Entry formatter
174
+ # used when matching entries. See {#entry_formatter}.
140
175
  def initialize(options = {})
141
176
  @buffer = []
142
177
  @max_entries = options[:max_entries] || 1000
178
+ @entry_formatter = options[:entry_formatter]
143
179
  @lock = Mutex.new
144
180
  @options = options.dup
145
181
  end
@@ -180,7 +216,7 @@ module Lumberjack
180
216
  # @return [Lumberjack::LogEntry, nil] The most recent log entry, or nil
181
217
  # if no entries have been captured yet
182
218
  def last_entry
183
- @buffer.last
219
+ @lock.synchronize { @buffer.last }
184
220
  end
185
221
 
186
222
  # Clear all captured log entries from the buffer. This method is useful
@@ -189,7 +225,7 @@ module Lumberjack
189
225
  #
190
226
  # @return [void]
191
227
  def clear
192
- @buffer = []
228
+ @lock.synchronize { @buffer = [] }
193
229
  nil
194
230
  end
195
231
 
@@ -230,6 +266,9 @@ module Lumberjack
230
266
  # { user: { id: value } }). Values can be exact matches or test framework matchers
231
267
  # @option options [String, Regexp, Object] :progname Pattern to match against
232
268
  # the program name that generated the log entry
269
+ # @option options [Lumberjack::EntryFormatter, Lumberjack::Logger] :formatter
270
+ # Formatter used to format filter values when a raw comparison fails. Defaults
271
+ # to the device {#entry_formatter}.
233
272
  #
234
273
  # @return [Boolean] True if any captured entries match all specified criteria,
235
274
  # false otherwise
@@ -281,6 +320,9 @@ module Lumberjack
281
320
  # log entry attributes. Supports nested matching using dot notation
282
321
  # @param progname [String, Regexp, Object, nil] Pattern to match against
283
322
  # the program name that generated the log entry
323
+ # @param formatter [Lumberjack::EntryFormatter, Lumberjack::Logger, nil] Formatter
324
+ # used to format filter values when a raw comparison fails. Defaults to the
325
+ # device {#entry_formatter}.
284
326
  #
285
327
  # @return [Lumberjack::LogEntry, nil] The first matching log entry, or nil
286
328
  # if no entries match the specified criteria
@@ -305,8 +347,14 @@ module Lumberjack
305
347
  # attributes: {"request.endpoint" => "/users", "response.status" => 200}
306
348
  # )
307
349
  # expect(api_entry.attributes["request.endpoint"]).to eq("/users")
308
- def match(message: nil, severity: nil, attributes: nil, progname: nil)
309
- matcher = LogEntryMatcher.new(message: message, severity: severity, attributes: attributes, progname: progname)
350
+ def match(message: nil, severity: nil, attributes: nil, progname: nil, formatter: nil)
351
+ matcher = LogEntryMatcher.new(
352
+ message: message,
353
+ severity: severity,
354
+ attributes: attributes,
355
+ progname: progname,
356
+ formatter: formatter || entry_formatter
357
+ )
310
358
  entries.detect { |entry| matcher.match?(entry) }
311
359
  end
312
360
 
@@ -327,10 +375,19 @@ module Lumberjack
327
375
  # log entry attributes. Supports nested matching using dot notation
328
376
  # @param progname [String, Regexp, Object, nil] Pattern to match against
329
377
  # the program name that generated the log entry
378
+ # @param formatter [Lumberjack::EntryFormatter, Lumberjack::Logger, nil] Formatter
379
+ # used to format filter values when a raw comparison fails. Defaults to the
380
+ # device {#entry_formatter}.
330
381
  # @return [Lumberjack::LogEntry, nil] The closest matching log entry, or nil
331
382
  # if no entries meet the minimum score threshold
332
- def closest_match(message: nil, severity: nil, attributes: nil, progname: nil)
333
- matcher = LogEntryMatcher.new(message: message, severity: severity, attributes: attributes, progname: progname)
383
+ def closest_match(message: nil, severity: nil, attributes: nil, progname: nil, formatter: nil)
384
+ matcher = LogEntryMatcher.new(
385
+ message: message,
386
+ severity: severity,
387
+ attributes: attributes,
388
+ progname: progname,
389
+ formatter: formatter || entry_formatter
390
+ )
334
391
  matcher.closest(entries)
335
392
  end
336
393
  end