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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +0 -17
- data/CHANGELOG.md +52 -0
- data/README.md +35 -0
- data/UPGRADE_GUIDE.md +1 -1
- data/VERSION +1 -1
- data/lib/lumberjack/attribute_formatter.rb +0 -51
- data/lib/lumberjack/context.rb +6 -1
- data/lib/lumberjack/context_locals.rb +10 -2
- data/lib/lumberjack/context_logger.rb +14 -4
- data/lib/lumberjack/device/buffer.rb +52 -37
- data/lib/lumberjack/device/multi.rb +2 -2
- data/lib/lumberjack/device/test.rb +70 -13
- data/lib/lumberjack/device/writer.rb +35 -12
- data/lib/lumberjack/device.rb +0 -2
- data/lib/lumberjack/device_registry.rb +7 -6
- data/lib/lumberjack/entry_formatter.rb +4 -2
- data/lib/lumberjack/formatter/structured_formatter.rb +4 -4
- data/lib/lumberjack/formatter/tags_formatter.rb +3 -3
- data/lib/lumberjack/formatter.rb +0 -11
- data/lib/lumberjack/formatter_registry.rb +7 -6
- data/lib/lumberjack/log_entry.rb +13 -46
- data/lib/lumberjack/log_entry_matcher/indifferent_hash.rb +83 -0
- data/lib/lumberjack/log_entry_matcher/score.rb +12 -4
- data/lib/lumberjack/log_entry_matcher.rb +229 -28
- data/lib/lumberjack/logger.rb +19 -185
- data/lib/lumberjack/template.rb +65 -42
- data/lib/lumberjack/template_registry.rb +7 -6
- data/lib/lumberjack/utils.rb +1 -26
- data/lib/lumberjack.rb +5 -25
- data/lumberjack.gemspec +0 -2
- metadata +3 -22
- data/lib/lumberjack/device/date_rolling_log_file.rb +0 -22
- data/lib/lumberjack/device/size_rolling_log_file.rb +0 -22
- data/lib/lumberjack/formatter/tagged_message.rb +0 -17
- data/lib/lumberjack/tag_context.rb +0 -15
- data/lib/lumberjack/tag_formatter.rb +0 -31
- data/lib/lumberjack/tags.rb +0 -36
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e2209d8f5b5a64c28765eb9e0756c2723354430017e8fc718ed1238678534f62
|
|
4
|
+
data.tar.gz: 3f7d07d946b9975962e9c25186cffab9a2529142430ecd1a833ba7a3bcb4dc9f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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
|
#
|
data/lib/lumberjack/context.rb
CHANGED
|
@@ -46,7 +46,12 @@ module Lumberjack
|
|
|
46
46
|
@default_severity = nil
|
|
47
47
|
|
|
48
48
|
if parent_context
|
|
49
|
-
|
|
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, :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
36
|
+
flush_needed = false
|
|
35
37
|
|
|
36
38
|
@lock.synchronize do
|
|
37
|
-
@
|
|
39
|
+
unless @closed
|
|
40
|
+
@entries << entry
|
|
41
|
+
flush_needed = @entries.size >= @size
|
|
42
|
+
end
|
|
38
43
|
end
|
|
39
44
|
|
|
40
|
-
flush if
|
|
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
|
-
|
|
53
|
+
call_before_flush
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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(
|
|
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(
|
|
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
|