lumberjack_capture_device 2.0.0 → 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/CHANGELOG.md +24 -1
- data/README.md +28 -8
- data/VERSION +1 -1
- data/lib/lumberjack/capture_device/include_log_entry_matcher.rb +162 -27
- data/lib/lumberjack/capture_device/rspec.rb +8 -10
- data/lib/lumberjack/capture_device.rb +71 -103
- data/lumberjack_capture_device.gemspec +1 -2
- metadata +4 -19
- data/lib/lumberjack/capture_device/entry_score.rb +0 -276
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3de3ac7d3bf941c8f6283c70f8bf55163eb36f91bea583836f296e7c5fa2cc3a
|
|
4
|
+
data.tar.gz: df6a39ef765975ff2f029d99e46aaef435c7675be707ea8effa08fc6a7369308
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c08cdc46f06da187f942b449a0a15551aefd2c55891cbb93d17ab3b0c8f281e169bddc340bb43a9a6fdfb11aeb6cc43a4a639f3417ab3819bea002d91515e3c
|
|
7
|
+
data.tar.gz: dfa277bb83a2710a9df87f893a35eaf58c266c8eb1d73e00be6a4f8fb1cf6233f7f552a86329b84e85459de8b87963143197d46e07d47f4136a605f793f0136b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `include?` now symbolizes filter keys and raises an `ArgumentError` on unrecognized filter keys. Previously filters with string keys or misspelled keys were silently ignored, causing the method to match any captured entry.
|
|
12
|
+
- `write_to_underlying_device` accepts an optional `attributes` keyword argument to add attributes to each entry as it is written.
|
|
13
|
+
- The `include_log_entry` matcher failure message now shows a diff of the closest matching entry against the expectation instead of just printing the entry. Severity, message, progname, and each attribute are shown with the expected value on a `-` line and the logged value on a `+` line so it is obvious which ones prevented the match. The comparison is done with `Lumberjack::LogEntryMatcher#diff` using the device's entry formatter, so the diff always agrees with the match. The diff is also available directly from `Lumberjack::CaptureDevice::IncludeLogEntryMatcher#entry_diff`.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- `extract` now matches filters with the device `entry_formatter` like `include?`, `match`, and `closest_match` do. Previously filter values were only compared to the formatted values captured on the entries, so unformatted values in the filters did not match.
|
|
18
|
+
- `capture_logger_around_example` now correctly adds the rspec metadata attributes to entries written to the underlying device when an example fails. Previously the attributes were silently dropped and building them raised a `NoMethodError` since `RSpec::Core::Example` does not have a `source_location` method; the example location is now recorded in the `rspec.location` attribute.
|
|
19
|
+
- The `max_entries` option is no longer ignored when initializing a `Lumberjack::CaptureDevice`.
|
|
20
|
+
- Fixed missing line break after "Closest match found:" in RSpec matcher failure messages.
|
|
21
|
+
- The `include_log_entry` matcher description no longer omits the expected attributes when a matcher (i.e. RSpec's `hash_including`) is passed as the `attributes` option instead of a hash. Matcher objects are now rendered with their description rather than by inspecting them.
|
|
22
|
+
- `each` and `length` now read from a thread safe copy of the entries buffer.
|
|
23
|
+
|
|
24
|
+
### Removed
|
|
25
|
+
|
|
26
|
+
- Removed the deprecated `:level` and `:tags` options from the matching methods (`include?`, `match`, `closest_match`, and `extract`) and from the `include_log_entry` RSpec matcher. Use `:severity` and `:attributes` instead.
|
|
27
|
+
- Removed the `match` and `closest_match` methods. They only existed to translate the deprecated options and are now inherited unchanged from `Lumberjack::Device::Test`.
|
|
28
|
+
- Removed unused internal `Lumberjack::CaptureDevice::EntryScore` class. It was never loaded and was replaced by the scoring logic in the lumberjack gem.
|
|
29
|
+
|
|
7
30
|
## 2.0.0
|
|
8
31
|
|
|
9
32
|
### Added
|
|
@@ -17,7 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
40
|
|
|
18
41
|
### Deprecated
|
|
19
42
|
|
|
20
|
-
- The `:level` and `:tags` options on the matching methods (`include?`, `match`, `closest_match`, and `extract`)
|
|
43
|
+
- The `:level` and `:tags` options on the matching methods (`include?`, `match`, `closest_match`, and `extract`) have been deprecated in favor of `:severity` and `:attributes`.
|
|
21
44
|
|
|
22
45
|
## 1.2.2
|
|
23
46
|
|
data/README.md
CHANGED
|
@@ -26,7 +26,7 @@ You can use the `include?` method on the log device to determine if specific log
|
|
|
26
26
|
```ruby
|
|
27
27
|
Lumberjack::CaptureDevice.capture(Rails.logger) do |logs|
|
|
28
28
|
do_something
|
|
29
|
-
expect(logs).to include(
|
|
29
|
+
expect(logs).to include(severity: :info, message: "Something happened")
|
|
30
30
|
end
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -34,30 +34,30 @@ You can also write that same test as:
|
|
|
34
34
|
|
|
35
35
|
```ruby
|
|
36
36
|
logs = Lumberjack::CaptureDevice.capture(Rails.logger) { do_something }
|
|
37
|
-
expect(logs).to include(
|
|
37
|
+
expect(logs).to include(severity: :info, message: "Something happened")
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
For MiniTest, you could assert:
|
|
41
41
|
|
|
42
42
|
```ruby
|
|
43
43
|
logs = Lumberjack::CaptureDevice.capture(Rails.logger) { do_something }
|
|
44
|
-
assert(logs.include?(
|
|
44
|
+
assert(logs.include?(severity: :info, message: "Something happened"))
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
You can filter the logs on
|
|
47
|
+
You can filter the logs on severity, message, progname, and attributes.
|
|
48
48
|
|
|
49
|
-
- The
|
|
49
|
+
- The severity option can take either a label (i.e. `:warn`) or a constant (i.e. `Logger::WARN`).
|
|
50
50
|
- The message filter can be either an exact string or a regular expression, or any matcher supported by your test library.
|
|
51
51
|
- The attributes argument can match attributes with a Hash mapping attribute names to the matcher values. If attributes are nested, you can use dot notation on attribute names to reference nested attributes.
|
|
52
52
|
|
|
53
53
|
```ruby
|
|
54
|
-
expect(logs).to include(
|
|
55
|
-
expect(logs).to include(
|
|
54
|
+
expect(logs).to include(severity: :info, message: /something/i)
|
|
55
|
+
expect(logs).to include(severity: Logger::INFO, attributes: {foo: "bar"})
|
|
56
56
|
expect(logs).to include(attributes: {foo: anything, count: {one: 1}})
|
|
57
57
|
expect(logs).to include(attributes: {foo: anything, "count.one" => 1})
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
You can also use the `Lumberjack::CaptureDevice#extract` method with the same arguments as used by `include?` to extract all log entries that match the filters. You can get all of the log entries with `Lumberjack::CaptureDevice#
|
|
60
|
+
You can also use the `Lumberjack::CaptureDevice#extract` method with the same arguments as used by `include?` to extract all log entries that match the filters. You can get all of the log entries with `Lumberjack::CaptureDevice#entries`.
|
|
61
61
|
|
|
62
62
|
### RSpec Support
|
|
63
63
|
|
|
@@ -78,6 +78,26 @@ describe MyClass do
|
|
|
78
78
|
end
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
When the matcher fails, the failure message includes a diff between the expectation and the closest matching entry so you can see exactly which fields and attributes are off. Lines prefixed with `-` show what was expected and lines prefixed with `+` show what was actually logged.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
expected logs to include entry:
|
|
85
|
+
severity: WARN
|
|
86
|
+
message: User logged out
|
|
87
|
+
attributes: request_id: "abc"
|
|
88
|
+
user.role: "admin"
|
|
89
|
+
|
|
90
|
+
Closest match found (- expected, + actual):
|
|
91
|
+
- severity: WARN
|
|
92
|
+
+ severity: INFO
|
|
93
|
+
message: "User logged out"
|
|
94
|
+
- attributes.user.role: "admin"
|
|
95
|
+
+ attributes.user.role: "guest"
|
|
96
|
+
attributes.duration: 1.5
|
|
97
|
+
- attributes.request_id: "abc"
|
|
98
|
+
+ attributes.request_id: (not set)
|
|
99
|
+
```
|
|
100
|
+
|
|
81
101
|
You can also set up log capturing around each example with the `capture_logger_around_example` method.
|
|
82
102
|
|
|
83
103
|
```ruby
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.1.0
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
# RSpec matcher for checking captured logs for specific entries.
|
|
4
4
|
class Lumberjack::CaptureDevice::IncludeLogEntryMatcher
|
|
5
|
+
# Displayed as the actual value for an expected attribute that the log entry does not have.
|
|
6
|
+
MISSING_VALUE = "(not set)"
|
|
7
|
+
|
|
8
|
+
private_constant :MISSING_VALUE
|
|
9
|
+
|
|
5
10
|
# Initialize the matcher with expected log entry attributes.
|
|
6
11
|
#
|
|
7
12
|
# @param expected_hash [Hash] Expected log entry attributes to match against.
|
|
8
13
|
def initialize(expected_hash)
|
|
14
|
+
# The keys are symbolized so the hash can be passed to any Lumberjack::Device::Test device.
|
|
9
15
|
@expected_hash = expected_hash.transform_keys(&:to_sym)
|
|
10
16
|
@logger = nil
|
|
11
17
|
end
|
|
@@ -19,7 +25,6 @@ class Lumberjack::CaptureDevice::IncludeLogEntryMatcher
|
|
|
19
25
|
@logger = actual
|
|
20
26
|
return false unless valid_logger?
|
|
21
27
|
|
|
22
|
-
device = @logger.is_a?(Lumberjack::Device::Test) ? @logger : @logger.device
|
|
23
28
|
device.include?(@expected_hash)
|
|
24
29
|
end
|
|
25
30
|
|
|
@@ -28,7 +33,7 @@ class Lumberjack::CaptureDevice::IncludeLogEntryMatcher
|
|
|
28
33
|
# @return [String] A formatted failure message.
|
|
29
34
|
def failure_message
|
|
30
35
|
if valid_logger?
|
|
31
|
-
formatted_failure_message(@
|
|
36
|
+
formatted_failure_message(@expected_hash)
|
|
32
37
|
else
|
|
33
38
|
wrong_object_type_message(@logger)
|
|
34
39
|
end
|
|
@@ -39,7 +44,7 @@ class Lumberjack::CaptureDevice::IncludeLogEntryMatcher
|
|
|
39
44
|
# @return [String] A formatted failure message for negated expectations.
|
|
40
45
|
def failure_message_when_negated
|
|
41
46
|
if valid_logger?
|
|
42
|
-
formatted_negated_failure_message(@
|
|
47
|
+
formatted_negated_failure_message(@expected_hash)
|
|
43
48
|
else
|
|
44
49
|
wrong_object_type_message(@logger)
|
|
45
50
|
end
|
|
@@ -52,6 +57,30 @@ class Lumberjack::CaptureDevice::IncludeLogEntryMatcher
|
|
|
52
57
|
"have logged entry with #{expectation_description(@expected_hash)}"
|
|
53
58
|
end
|
|
54
59
|
|
|
60
|
+
# Generate a diff between a log entry and the expected log entry values. The severity,
|
|
61
|
+
# message, progname, and attributes of the entry are listed. Values that don't match the
|
|
62
|
+
# expectation are shown on a pair of lines with the expected value prefixed with "-" and
|
|
63
|
+
# the value from the log entry prefixed with "+". This is intended to make it easy to spot
|
|
64
|
+
# exactly which fields kept an entry from matching.
|
|
65
|
+
#
|
|
66
|
+
# The comparison is done by Lumberjack::LogEntryMatcher#diff so the diff always agrees
|
|
67
|
+
# with the result of the match.
|
|
68
|
+
#
|
|
69
|
+
# @param entry [Lumberjack::LogEntry] The log entry to compare to the expectation.
|
|
70
|
+
# @param indent [Integer] The number of spaces to indent each line.
|
|
71
|
+
# @return [String] A formatted diff of the entry and the expectation.
|
|
72
|
+
def entry_diff(entry, indent: 2)
|
|
73
|
+
indent_str = " " * indent
|
|
74
|
+
|
|
75
|
+
entry_differences(entry).collect do |name, expected, actual, matched|
|
|
76
|
+
if matched
|
|
77
|
+
"#{indent_str} #{name}: #{actual}"
|
|
78
|
+
else
|
|
79
|
+
"#{indent_str}- #{name}: #{expected}#{Lumberjack::LINE_SEPARATOR}#{indent_str}+ #{name}: #{actual}"
|
|
80
|
+
end
|
|
81
|
+
end.join(Lumberjack::LINE_SEPARATOR)
|
|
82
|
+
end
|
|
83
|
+
|
|
55
84
|
private
|
|
56
85
|
|
|
57
86
|
# Check if the logger is using a valid Lumberjack::Device::Test device.
|
|
@@ -79,27 +108,16 @@ class Lumberjack::CaptureDevice::IncludeLogEntryMatcher
|
|
|
79
108
|
|
|
80
109
|
# Generate a detailed failure message showing expected vs actual logs.
|
|
81
110
|
#
|
|
82
|
-
# @param logger_or_device [Lumberjack::Device::Test] The logger device.
|
|
83
111
|
# @param expected_hash [Hash] The expected log entry attributes.
|
|
84
112
|
# @return [String] A formatted failure message with context.
|
|
85
|
-
def formatted_failure_message(
|
|
86
|
-
device = logger_or_device.respond_to?(:device) ? logger_or_device.device : logger_or_device
|
|
87
|
-
|
|
88
|
-
# Handle deprecated keys
|
|
89
|
-
if expected_hash.include?(:level) && !expected_hash.include?(:severity)
|
|
90
|
-
expected_hash = expected_hash.merge(severity: expected_hash[:level])
|
|
91
|
-
end
|
|
92
|
-
if expected_hash.include?(:tags) && !expected_hash.include?(:attributes)
|
|
93
|
-
expected_hash = expected_hash.merge(attributes: expected_hash[:tags])
|
|
94
|
-
end
|
|
95
|
-
|
|
113
|
+
def formatted_failure_message(expected_hash)
|
|
96
114
|
message = +"expected logs to include entry:\n" \
|
|
97
115
|
"#{Lumberjack::Device::Test.formatted_expectation(expected_hash, indent: 2)}"
|
|
98
116
|
|
|
99
117
|
closest_match = device.closest_match(**expected_hash)
|
|
100
118
|
if closest_match
|
|
101
|
-
message << "\n\nClosest match found
|
|
102
|
-
"#{
|
|
119
|
+
message << "\n\nClosest match found (- expected, + actual):\n" \
|
|
120
|
+
"#{entry_diff(closest_match, indent: 2)}"
|
|
103
121
|
end
|
|
104
122
|
|
|
105
123
|
entries = device.entries
|
|
@@ -117,11 +135,9 @@ class Lumberjack::CaptureDevice::IncludeLogEntryMatcher
|
|
|
117
135
|
|
|
118
136
|
# Generate a failure message for negated expectations.
|
|
119
137
|
#
|
|
120
|
-
# @param logger_or_device [Lumberjack::Device::Test] The logger to check.
|
|
121
138
|
# @param expected_hash [Hash] The expected log entry attributes that should not be present.
|
|
122
139
|
# @return [String] A formatted failure message for negated expectations.
|
|
123
|
-
def formatted_negated_failure_message(
|
|
124
|
-
device = logger_or_device.respond_to?(:device) ? logger_or_device.device : logger_or_device
|
|
140
|
+
def formatted_negated_failure_message(expected_hash)
|
|
125
141
|
message = "expected logs not to include entry:\n" \
|
|
126
142
|
"#{Lumberjack::Device::Test.formatted_expectation(expected_hash, indent: 2)}"
|
|
127
143
|
|
|
@@ -140,14 +156,133 @@ class Lumberjack::CaptureDevice::IncludeLogEntryMatcher
|
|
|
140
156
|
# @return [String] A formatted description of the expected attributes.
|
|
141
157
|
def expectation_description(expected_hash)
|
|
142
158
|
info = []
|
|
143
|
-
info << "severity: #{expected_hash[:severity]
|
|
144
|
-
info << "message: #{expected_hash[:message]
|
|
145
|
-
info << "progname: #{expected_hash[:progname]
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
info << "severity: #{formatted_value(expected_hash[:severity])}" unless expected_hash[:severity].nil?
|
|
160
|
+
info << "message: #{formatted_value(expected_hash[:message])}" unless expected_hash[:message].nil?
|
|
161
|
+
info << "progname: #{formatted_value(expected_hash[:progname])}" unless expected_hash[:progname].nil?
|
|
162
|
+
|
|
163
|
+
expected_attributes = expected_hash[:attributes]
|
|
164
|
+
if expected_attributes.is_a?(Hash)
|
|
165
|
+
unless expected_attributes.empty?
|
|
166
|
+
attributes = Lumberjack::Utils.flatten_attributes(expected_attributes)
|
|
167
|
+
attributes_info = attributes.collect { |name, value| "#{name}=#{formatted_value(value)}" }.join(", ")
|
|
168
|
+
info << "attributes: #{attributes_info}"
|
|
169
|
+
end
|
|
170
|
+
elsif expected_attributes
|
|
171
|
+
# Matchers like RSpec's hash_including are matched against the attributes hash as a whole.
|
|
172
|
+
info << "attributes: #{formatted_value(expected_attributes)}"
|
|
150
173
|
end
|
|
174
|
+
|
|
151
175
|
info.join(", ")
|
|
152
176
|
end
|
|
177
|
+
|
|
178
|
+
# Build the matcher used to compare log entries to the expectation. The entry formatter
|
|
179
|
+
# from the device is used so filter values are matched the same way they are by the
|
|
180
|
+
# device itself.
|
|
181
|
+
#
|
|
182
|
+
# @return [Lumberjack::LogEntryMatcher] The matcher for the expected values.
|
|
183
|
+
def log_entry_matcher
|
|
184
|
+
Lumberjack::LogEntryMatcher.new(
|
|
185
|
+
message: @expected_hash[:message],
|
|
186
|
+
severity: @expected_hash[:severity],
|
|
187
|
+
progname: @expected_hash[:progname],
|
|
188
|
+
attributes: @expected_hash[:attributes],
|
|
189
|
+
formatter: device&.entry_formatter
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Compare a log entry to the expected values one field at a time. The comparison itself is
|
|
194
|
+
# done by Lumberjack::LogEntryMatcher#diff which reports only the mismatched fields; the
|
|
195
|
+
# rest of the entry is filled in from the entry so it can be seen in full.
|
|
196
|
+
#
|
|
197
|
+
# @param entry [Lumberjack::LogEntry] The log entry to compare to the expectation.
|
|
198
|
+
# @return [Array<Array>] An array of [name, expected value, actual value, matched] tuples
|
|
199
|
+
# in the order they should be displayed. The values are already formatted for display.
|
|
200
|
+
def entry_differences(entry)
|
|
201
|
+
diff = log_entry_matcher.diff(entry)
|
|
202
|
+
differences = []
|
|
203
|
+
|
|
204
|
+
mismatch = diff["severity"]
|
|
205
|
+
differences << if mismatch
|
|
206
|
+
# Severities are already reported as labels by the diff.
|
|
207
|
+
["severity", mismatch[:expected].to_s, mismatch[:actual].to_s, false]
|
|
208
|
+
else
|
|
209
|
+
["severity", nil, entry.severity_label, true]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
differences << field_difference("message", diff["message"], entry.message)
|
|
213
|
+
|
|
214
|
+
unless diff["progname"].nil? && entry.progname.nil?
|
|
215
|
+
differences << field_difference("progname", diff["progname"], entry.progname)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
differences.concat(attribute_differences(entry, diff["attributes"]))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Build the display tuple for a single log entry field.
|
|
222
|
+
#
|
|
223
|
+
# @param name [String] The name of the field.
|
|
224
|
+
# @param mismatch [Hash, nil] The expected and actual values reported by the diff, or nil
|
|
225
|
+
# if the field matched the expectation.
|
|
226
|
+
# @param value [Object] The value from the log entry, used when the field matched.
|
|
227
|
+
# @return [Array] A [name, expected value, actual value, matched] tuple.
|
|
228
|
+
def field_difference(name, mismatch, value)
|
|
229
|
+
if mismatch
|
|
230
|
+
[name, formatted_value(mismatch[:expected]), formatted_value(mismatch[:actual]), false]
|
|
231
|
+
else
|
|
232
|
+
[name, nil, formatted_value(value), true]
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Build the display tuples for the attributes of a log entry. Mismatches are reported by
|
|
237
|
+
# the diff per attribute using dot notation names. The remaining attributes on the entry
|
|
238
|
+
# are listed as well, followed by any expected attributes the entry does not have.
|
|
239
|
+
#
|
|
240
|
+
# @param entry [Lumberjack::LogEntry] The log entry being compared to the expectation.
|
|
241
|
+
# @param mismatches [Hash, nil] The attribute mismatches reported by the diff.
|
|
242
|
+
# @return [Array<Array>] An array of [name, expected value, actual value, matched] tuples.
|
|
243
|
+
def attribute_differences(entry, mismatches)
|
|
244
|
+
mismatches ||= {}
|
|
245
|
+
|
|
246
|
+
if mismatches.include?(:expected)
|
|
247
|
+
# Matchers like RSpec's hash_including are matched against the attributes hash as a whole.
|
|
248
|
+
return [field_difference("attributes", mismatches, nil)]
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
entry_attributes = Lumberjack::Utils.flatten_attributes(entry.attributes || {})
|
|
252
|
+
|
|
253
|
+
differences = entry_attributes.collect do |name, value|
|
|
254
|
+
field_difference("attributes.#{name}", mismatches[name], value)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
(mismatches.keys - entry_attributes.keys).each do |name|
|
|
258
|
+
mismatch = mismatches[name]
|
|
259
|
+
actual = mismatch[:actual].nil? ? MISSING_VALUE : formatted_value(mismatch[:actual])
|
|
260
|
+
differences << ["attributes.#{name}", formatted_value(mismatch[:expected]), actual, false]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
differences
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# The Lumberjack::Device::Test the entries are being matched against.
|
|
267
|
+
#
|
|
268
|
+
# @return [Lumberjack::Device::Test, nil] The device, or nil if the logger is not valid.
|
|
269
|
+
def device
|
|
270
|
+
return nil unless valid_logger?
|
|
271
|
+
|
|
272
|
+
@logger.is_a?(Lumberjack::Device::Test) ? @logger : @logger.device
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Format a value for display in a description. Matcher objects (i.e. RSpec matchers)
|
|
276
|
+
# that implement a +description+ method are displayed using that description since
|
|
277
|
+
# inspecting them is not very informative.
|
|
278
|
+
#
|
|
279
|
+
# @param value [Object] The value to format.
|
|
280
|
+
# @return [String] The formatted value.
|
|
281
|
+
def formatted_value(value)
|
|
282
|
+
if value.respond_to?(:description) && !value.is_a?(Module)
|
|
283
|
+
value.description.to_s
|
|
284
|
+
else
|
|
285
|
+
value.inspect
|
|
286
|
+
end
|
|
287
|
+
end
|
|
153
288
|
end
|
|
@@ -9,15 +9,13 @@ module Lumberjack::CaptureDevice::RSpec
|
|
|
9
9
|
# This matcher provides better error messages than using the include? method directly.
|
|
10
10
|
#
|
|
11
11
|
# @param expected_hash [Hash] The expected log entry attributes to match.
|
|
12
|
-
# @option expected_hash [String, Symbol, Integer] :
|
|
13
|
-
# @option expected_hash [String, Symbol, Integer] :severity Alias for :level.
|
|
12
|
+
# @option expected_hash [String, Symbol, Integer] :severity The expected log severity.
|
|
14
13
|
# @option expected_hash [String, Regexp] :message The expected message content.
|
|
15
14
|
# @option expected_hash [Hash] :attributes Expected log entry attributes.
|
|
16
|
-
# @option expected_hash [Hash] :tags Alias for :attributes.
|
|
17
15
|
# @option expected_hash [String] :progname Expected program name.
|
|
18
16
|
# @return [Lumberjack::CaptureDevice::IncludeLogEntryMatcher] A matcher for the expected log entry.
|
|
19
17
|
# @example
|
|
20
|
-
# expect(logs).to include_log_entry(
|
|
18
|
+
# expect(logs).to include_log_entry(severity: :info, message: "User logged in")
|
|
21
19
|
# @example
|
|
22
20
|
# expect(logs).to include_log_entry(message: /error/i, attributes: {user_id: 123})
|
|
23
21
|
def include_log_entry(expected_hash)
|
|
@@ -25,8 +23,9 @@ module Lumberjack::CaptureDevice::RSpec
|
|
|
25
23
|
end
|
|
26
24
|
|
|
27
25
|
# Capture log entries from a logger within a block. This method temporarily
|
|
28
|
-
# replaces the logger's device with a CaptureDevice
|
|
29
|
-
#
|
|
26
|
+
# replaces the logger's device with a CaptureDevice and sets the log level to debug.
|
|
27
|
+
# The logger's formatters remain active, so captured entries contain the same
|
|
28
|
+
# formatted values that would have been logged.
|
|
30
29
|
#
|
|
31
30
|
# @param logger [Lumberjack::Logger] The logger to capture entries from.
|
|
32
31
|
# @yield [device] The block to execute while capturing log entries.
|
|
@@ -36,7 +35,7 @@ module Lumberjack::CaptureDevice::RSpec
|
|
|
36
35
|
# logs = capture_logger(Rails.logger) do
|
|
37
36
|
# Rails.logger.info("Test message")
|
|
38
37
|
# end
|
|
39
|
-
# expect(logs).to include_log_entry(
|
|
38
|
+
# expect(logs).to include_log_entry(severity: :info, message: "Test message")
|
|
40
39
|
def capture_logger(logger, write_to_original: true, &block)
|
|
41
40
|
Lumberjack::CaptureDevice.capture(logger, write_to_original: write_to_original, &block)
|
|
42
41
|
end
|
|
@@ -62,9 +61,8 @@ module Lumberjack::CaptureDevice::RSpec
|
|
|
62
61
|
example.run
|
|
63
62
|
|
|
64
63
|
if example.exception
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
end
|
|
64
|
+
rspec_attributes = {rspec: {location: example.location, description: example.metadata[:description]}}
|
|
65
|
+
captured_device.write_to_underlying_device(attributes: rspec_attributes)
|
|
68
66
|
end
|
|
69
67
|
end
|
|
70
68
|
end
|
|
@@ -15,9 +15,16 @@ module Lumberjack
|
|
|
15
15
|
class << self
|
|
16
16
|
# Capture the entries written by the logger within a block. Within the block all log
|
|
17
17
|
# entries will be written to a CaptureDevice rather than to the normal output for
|
|
18
|
-
# the logger. In addition,
|
|
19
|
-
#
|
|
20
|
-
#
|
|
18
|
+
# the logger. In addition, the log level will be set to debug. The logger's formatters
|
|
19
|
+
# remain active, so captured entries contain the same formatted values that would have
|
|
20
|
+
# been logged. The device being written to be both yielded to the block as well as
|
|
21
|
+
# returned by the method call.
|
|
22
|
+
#
|
|
23
|
+
# This method is not thread safe. It swaps the device and log level on the logger
|
|
24
|
+
# itself, so concurrent calls on the same logger will interfere with each other and
|
|
25
|
+
# entries logged by other threads during the block will be captured as well. The log
|
|
26
|
+
# level is set on the current context, so threads spawned within the block may not
|
|
27
|
+
# log at the debug level.
|
|
21
28
|
#
|
|
22
29
|
# @param logger [Lumberjack::Logger] The logger to capture entries from.
|
|
23
30
|
# @param write_to_original [Boolean] If true (the default) the captured entries will be written
|
|
@@ -64,11 +71,20 @@ module Lumberjack
|
|
|
64
71
|
# @param options [Hash] Options to pass to the parent Test device.
|
|
65
72
|
def initialize(options = {})
|
|
66
73
|
@underlying_device = options[:underlying_device]
|
|
67
|
-
super(
|
|
74
|
+
super({max_entries: 1_000_000}.merge(options))
|
|
68
75
|
end
|
|
69
76
|
|
|
70
|
-
# Return all the captured entries that match the specified filters.
|
|
71
|
-
#
|
|
77
|
+
# Return all the captured entries that match the specified filters. The device
|
|
78
|
+
# `entry_formatter` is used to match the filters, so unformatted values can be used in
|
|
79
|
+
# the filters if it is set.
|
|
80
|
+
#
|
|
81
|
+
# For severity, you can specify either a numeric constant (i.e. `Logger::WARN`) or a symbol
|
|
82
|
+
# (i.e. `:warn`).
|
|
83
|
+
#
|
|
84
|
+
# For message and progname you can specify a string to perform an exact match or a regular
|
|
85
|
+
# expression to perform a partial or pattern match. You can also supply any matcher value
|
|
86
|
+
# available in your test library (i.e. in rspec you could use `anything` or `instance_of(Error)`,
|
|
87
|
+
# etc.).
|
|
72
88
|
#
|
|
73
89
|
# @param message [String, Regexp, nil] The message to match against the log entries.
|
|
74
90
|
# @param severity [String, Symbol, Integer, nil] The severity to match against the log entries.
|
|
@@ -78,21 +94,19 @@ module Lumberjack
|
|
|
78
94
|
# @param progname [String, nil] The program name to match against the log entries.
|
|
79
95
|
# @param limit [Integer, nil] The maximum number of entries to return. If nil, all matching entries
|
|
80
96
|
# will be returned.
|
|
81
|
-
# @param level [String, Symbol, Integer, nil] Alias for the `severity` parameter.
|
|
82
|
-
# @param tags [Hash, nil] Alias for the `attributes` parameter.
|
|
83
97
|
# @return [Array<Lumberjack::LogEntry>] An array of log entries that match the specified filters.
|
|
84
|
-
|
|
98
|
+
# @example
|
|
99
|
+
# logs.extract(severity: :warn, message: /something happened/, attributes: {user: "john"})
|
|
100
|
+
def extract(message: nil, severity: nil, attributes: nil, progname: nil, limit: nil)
|
|
85
101
|
matched = []
|
|
86
|
-
if severity.nil? && !level.nil?
|
|
87
|
-
Lumberjack::Utils.deprecated("Lumberjack::CaptureDevice#extract(level)", "Lumberjack::CaptureDevice#extract level parameter has been renamed to severity; it will be removed in version 2.1.")
|
|
88
|
-
severity = level
|
|
89
|
-
end
|
|
90
|
-
if attributes.nil? && !tags.nil?
|
|
91
|
-
Lumberjack::Utils.deprecated("Lumberjack::CaptureDevice#extract(tags)", "Lumberjack::CaptureDevice#extract tags parameter has been renamed to attributes; it will be removed in version 2.1.")
|
|
92
|
-
attributes = tags
|
|
93
|
-
end
|
|
94
102
|
|
|
95
|
-
matcher = LogEntryMatcher.new(
|
|
103
|
+
matcher = LogEntryMatcher.new(
|
|
104
|
+
message: message,
|
|
105
|
+
severity: severity,
|
|
106
|
+
attributes: attributes,
|
|
107
|
+
progname: progname,
|
|
108
|
+
formatter: entry_formatter
|
|
109
|
+
)
|
|
96
110
|
|
|
97
111
|
entries.each do |entry|
|
|
98
112
|
matched << entry if matcher.match?(entry)
|
|
@@ -102,108 +116,53 @@ module Lumberjack
|
|
|
102
116
|
matched
|
|
103
117
|
end
|
|
104
118
|
|
|
105
|
-
# Return true if the captured log entries match the specified
|
|
106
|
-
#
|
|
107
|
-
# For level, you can specify either a numeric constant (i.e. `Logger::WARN`) or a symbol
|
|
108
|
-
# (i.e. `:warn`).
|
|
119
|
+
# Return true if the captured log entries match the specified filters. The filters are the
|
|
120
|
+
# same as the ones used by the `extract` method.
|
|
109
121
|
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# in your test library (i.e. in rspec you could use `anything` or `instance_of(Error)`, etc.).
|
|
113
|
-
#
|
|
114
|
-
# For attributes, you can specify a hash of attribute names to values to match. You can use
|
|
115
|
-
# regular expression or matchers as the values here as well. attributes can also be nested to match
|
|
116
|
-
# nested attributes.
|
|
122
|
+
# This must be redefined here because Enumerable#include? would otherwise shadow the
|
|
123
|
+
# implementation inherited from Lumberjack::Device::Test.
|
|
117
124
|
#
|
|
118
125
|
# @example
|
|
119
|
-
# logs.include?(
|
|
126
|
+
# logs.include?(severity: :warn, message: /something happened/, attributes: {user: "john"})
|
|
120
127
|
#
|
|
121
128
|
# @param filters [Hash] The filters to apply to the captured entries.
|
|
122
129
|
# @option filters [String, Regexp] :message The message to match against the log entries.
|
|
123
|
-
# @option filters [String, Symbol, Integer] :severity The
|
|
130
|
+
# @option filters [String, Symbol, Integer] :severity The severity to match against the log entries.
|
|
124
131
|
# @option filters [Hash] :attributes A hash of attribute names to values to match against the log entries. The attributes
|
|
125
132
|
# will match nested attributes using dot notation (e.g. `foo.bar` will match an attribute with the structure
|
|
126
133
|
# +{foo: {bar: "value"}}+).
|
|
127
134
|
# @option filters [String] :progname The program name to match against the log entries.
|
|
128
|
-
# @option filters [String, Symbol, Integer, nil] :level Alias for the `severity` option. This option is deprecated.
|
|
129
|
-
# @option filters [Hash, nil] :tags Alias for the `attributes` option. This option is deprecated.
|
|
130
135
|
# @return [Boolean] True if any entries match the specified filters, false otherwise.
|
|
131
136
|
def include?(filters)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if filters.include?(:tags)
|
|
137
|
-
Lumberjack::Utils.deprecated("Lumberjack::CaptureDevice#include?(tags)", "Lumberjack::CaptureDevice#include? tags option has been renamed to attributes; it will be removed in version 2.1.")
|
|
137
|
+
filters = filters.transform_keys(&:to_sym)
|
|
138
|
+
unknown_keys = filters.keys - [:message, :severity, :attributes, :progname]
|
|
139
|
+
unless unknown_keys.empty?
|
|
140
|
+
raise ArgumentError, "unknown log filters: #{unknown_keys.map(&:inspect).join(", ")}"
|
|
138
141
|
end
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
message: filters[:message],
|
|
142
|
-
severity: filters[:severity] || filters[:level],
|
|
143
|
-
attributes: filters[:attributes] || filters[:tags],
|
|
144
|
-
progname: filters[:progname]
|
|
145
|
-
}.compact
|
|
146
|
-
|
|
147
|
-
!!match(**munged_filters)
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Return the first captured entry that matches the filters.
|
|
151
|
-
#
|
|
152
|
-
# @param message [String, Regexp, nil] The message to match against the log entries.
|
|
153
|
-
# @param severity [String, Symbol, Integer, nil] The log level to match against the log entries.
|
|
154
|
-
# @param attributes [Hash, nil] A hash of attribute names to values to match against the log entries. The attributes
|
|
155
|
-
# will match nested attributes using dot notation (e.g. `foo.bar` will match an attribute with the structure
|
|
156
|
-
# +{foo: {bar: "value"}}+).
|
|
157
|
-
# @param progname [String, nil] The program name to match against the log entries.
|
|
158
|
-
# @param level [String, Symbol, Integer, nil] Alias for the `severity` parameter. This parameter is deprecated.
|
|
159
|
-
# @param tags [Hash, nil] Alias for the `attributes` parameter. This parameter is deprecated.
|
|
160
|
-
# @return [Lumberjack::LogEntry, nil] The first matching log entry, or nil if no match is found.
|
|
161
|
-
def match(message: nil, severity: nil, attributes: nil, progname: nil, level: nil, tags: nil)
|
|
162
|
-
unless level.nil?
|
|
163
|
-
Lumberjack::Utils.deprecated("Lumberjack::CaptureDevice#match(level)", "Lumberjack::CaptureDevice#match level parameter has been renamed to severity; it will be removed in version 2.1.")
|
|
164
|
-
end
|
|
165
|
-
unless tags.nil?
|
|
166
|
-
Lumberjack::Utils.deprecated("Lumberjack::CaptureDevice#match(tags)", "Lumberjack::CaptureDevice#match tags parameter has been renamed to attributes; it will be removed in version 2.1.")
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
super(message: message, severity: severity || level, attributes: attributes || tags, progname: progname)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Return the log entry that most closely matches the specified filters. This method
|
|
173
|
-
# uses fuzzy matching logic to find the best match when no exact match exists.
|
|
174
|
-
# The matching score is calculated based on how many criteria are met and how closely
|
|
175
|
-
# they match. Returns nil if no entry meets the minimum matching criteria.
|
|
176
|
-
#
|
|
177
|
-
# @param message [String, Regexp, nil] The message to match against the log entries.
|
|
178
|
-
# @param severity [String, Symbol, Integer, nil] The severity to match against the log entries.
|
|
179
|
-
# @param attributes [Hash, nil] A hash of attribute names to values to match against the log entries.
|
|
180
|
-
# @param progname [String, nil] The program name to match against the log entries.
|
|
181
|
-
# @param level [String, Symbol, Integer, nil] Alias for the `severity` parameter.
|
|
182
|
-
# @param tags [Hash, nil] Alias for the `attributes` parameter.
|
|
183
|
-
# @return [Lumberjack::LogEntry, nil] The log entry that most closely matches the filters, or nil if no entry meets minimum criteria.
|
|
184
|
-
def closest_match(message: nil, severity: nil, attributes: nil, progname: nil, level: nil, tags: nil)
|
|
185
|
-
return nil if length == 0
|
|
186
|
-
|
|
187
|
-
unless level.nil?
|
|
188
|
-
Lumberjack::Utils.deprecated("Lumberjack::CaptureDevice#closest_match(level)", "Lumberjack::CaptureDevice#closest_match level parameter has been renamed to severity; it will be removed in version 2.1.")
|
|
189
|
-
end
|
|
190
|
-
unless tags.nil?
|
|
191
|
-
Lumberjack::Utils.deprecated("Lumberjack::CaptureDevice#closest_match(tags)", "Lumberjack::CaptureDevice#closest_match tags parameter has been renamed to attributes; it will be removed in version 2.1.")
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
super(
|
|
195
|
-
message: message,
|
|
196
|
-
severity: severity || level,
|
|
197
|
-
attributes: attributes || tags,
|
|
198
|
-
progname: progname
|
|
199
|
-
)
|
|
143
|
+
!!match(**filters)
|
|
200
144
|
end
|
|
201
145
|
|
|
202
146
|
# Write the captured log entries to the underlying device.
|
|
203
147
|
#
|
|
148
|
+
# @param attributes [Hash, nil] Additional attributes to add to each entry as it is
|
|
149
|
+
# written. Attributes already set on an entry take precedence over these values.
|
|
204
150
|
# @return [void]
|
|
205
|
-
def write_to_underlying_device
|
|
206
|
-
|
|
151
|
+
def write_to_underlying_device(attributes: nil)
|
|
152
|
+
return unless @underlying_device
|
|
153
|
+
|
|
154
|
+
if attributes.nil? || attributes.empty?
|
|
155
|
+
write_to(@underlying_device)
|
|
156
|
+
else
|
|
157
|
+
extra_attributes = Lumberjack::Utils.expand_attributes(attributes)
|
|
158
|
+
entries.each do |entry|
|
|
159
|
+
copy = entry.dup
|
|
160
|
+
copy.attributes = extra_attributes.merge(Lumberjack::Utils.expand_attributes(entry.attributes || {}))
|
|
161
|
+
@underlying_device.write(copy)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
nil
|
|
207
166
|
end
|
|
208
167
|
|
|
209
168
|
# Provide a detailed string representation showing all captured entries.
|
|
@@ -228,11 +187,20 @@ module Lumberjack
|
|
|
228
187
|
"<##{self.class.name} #{length} #{(length == 1) ? "entry" : "entries"} captured>"
|
|
229
188
|
end
|
|
230
189
|
|
|
190
|
+
# Return a thread-safe copy of all captured log entries. This must be redefined here
|
|
191
|
+
# because Enumerable#entries would otherwise shadow the thread-safe implementation
|
|
192
|
+
# inherited from Lumberjack::Device::Test.
|
|
193
|
+
#
|
|
194
|
+
# @return [Array<Lumberjack::LogEntry>] A copy of all captured log entries.
|
|
195
|
+
def entries
|
|
196
|
+
@lock.synchronize { @buffer.dup }
|
|
197
|
+
end
|
|
198
|
+
|
|
231
199
|
# Return the number of captured log entries.
|
|
232
200
|
#
|
|
233
201
|
# @return [Integer] The number of captured entries.
|
|
234
202
|
def length
|
|
235
|
-
|
|
203
|
+
entries.length
|
|
236
204
|
end
|
|
237
205
|
|
|
238
206
|
alias_method :size, :length
|
|
@@ -243,7 +211,7 @@ module Lumberjack
|
|
|
243
211
|
# @yieldparam entry [Lumberjack::LogEntry] A captured log entry.
|
|
244
212
|
# @return [Array<Lumberjack::LogEntry>] The captured entries (when no block given).
|
|
245
213
|
def each(&block)
|
|
246
|
-
|
|
214
|
+
entries.each(&block)
|
|
247
215
|
end
|
|
248
216
|
end
|
|
249
217
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lumberjack_capture_device
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
@@ -15,28 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: 2.1.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: bundler
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
33
|
-
type: :development
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
25
|
+
version: 2.1.0
|
|
40
26
|
email:
|
|
41
27
|
- bbdurand@gmail.com
|
|
42
28
|
executables: []
|
|
@@ -48,7 +34,6 @@ files:
|
|
|
48
34
|
- README.md
|
|
49
35
|
- VERSION
|
|
50
36
|
- lib/lumberjack/capture_device.rb
|
|
51
|
-
- lib/lumberjack/capture_device/entry_score.rb
|
|
52
37
|
- lib/lumberjack/capture_device/include_log_entry_matcher.rb
|
|
53
38
|
- lib/lumberjack/capture_device/rspec.rb
|
|
54
39
|
- lib/lumberjack_capture_device.rb
|
|
@@ -74,7 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
74
59
|
- !ruby/object:Gem::Version
|
|
75
60
|
version: '0'
|
|
76
61
|
requirements: []
|
|
77
|
-
rubygems_version:
|
|
62
|
+
rubygems_version: 4.0.3
|
|
78
63
|
specification_version: 4
|
|
79
64
|
summary: Testing device for the lumberjack gem that can be used for asserting messages
|
|
80
65
|
have been logged in a test suite.
|
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Class responsible for scoring and matching log entries against filters.
|
|
4
|
-
# This class provides fuzzy matching capabilities to find the best matching
|
|
5
|
-
# log entry when exact matches are not available.
|
|
6
|
-
class Lumberjack::CaptureDevice::EntryScore
|
|
7
|
-
# Minimum score threshold for considering a match (30% match)
|
|
8
|
-
MIN_SCORE_THRESHOLD = 0.3
|
|
9
|
-
|
|
10
|
-
class << self
|
|
11
|
-
# Calculate the overall match score for an entry against all provided filters.
|
|
12
|
-
# Returns a score between 0.0 and 1.0, where 1.0 represents a perfect match.
|
|
13
|
-
#
|
|
14
|
-
# @param entry [Lumberjack::LogEntry] The log entry to score.
|
|
15
|
-
# @param message_filter [String, Regexp, nil] The message filter to match against.
|
|
16
|
-
# @param severity_filter [Integer, nil] The severity level to match against.
|
|
17
|
-
# @param attributes_filter [Hash, nil] The attributes hash to match against.
|
|
18
|
-
# @param progname_filter [String, nil] The program name to match against.
|
|
19
|
-
# @return [Float] A score between 0.0 and 1.0 indicating match quality.
|
|
20
|
-
def calculate_match_score(entry, message_filter, severity_filter, attributes_filter, progname_filter)
|
|
21
|
-
scores = []
|
|
22
|
-
weights = []
|
|
23
|
-
|
|
24
|
-
# Check message match
|
|
25
|
-
if message_filter
|
|
26
|
-
message_score = calculate_field_score(entry.message, message_filter)
|
|
27
|
-
scores << message_score
|
|
28
|
-
weights << 0.4 # Weight message matching highly
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Check severity match
|
|
32
|
-
if severity_filter
|
|
33
|
-
severity_score = if entry.severity == severity_filter
|
|
34
|
-
1.0 # Exact severity match
|
|
35
|
-
else
|
|
36
|
-
severity_proximity_score(entry.severity, severity_filter) # Partial severity match
|
|
37
|
-
end
|
|
38
|
-
scores << severity_score
|
|
39
|
-
weights << 0.3
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Check progname match
|
|
43
|
-
if progname_filter
|
|
44
|
-
progname_score = calculate_field_score(entry.progname, progname_filter)
|
|
45
|
-
scores << progname_score
|
|
46
|
-
weights << 0.2
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Check attributes match
|
|
50
|
-
if attributes_filter.is_a?(Hash) && !attributes_filter.empty?
|
|
51
|
-
attributes_score = calculate_attributes_score(entry.attributes, attributes_filter)
|
|
52
|
-
scores << attributes_score
|
|
53
|
-
weights << 0.3
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Return 0 if no criteria were provided
|
|
57
|
-
return 0.0 if scores.empty?
|
|
58
|
-
|
|
59
|
-
# Calculate weighted average, but apply a penalty if any score is 0
|
|
60
|
-
# This ensures that completely failed criteria significantly impact the result
|
|
61
|
-
total_weighted_score = scores.zip(weights).map { |score, weight| score * weight }.sum
|
|
62
|
-
total_weight = weights.sum
|
|
63
|
-
base_score = total_weighted_score / total_weight
|
|
64
|
-
|
|
65
|
-
# Apply penalty for zero scores: reduce the score based on how many criteria completely failed
|
|
66
|
-
zero_scores = scores.count(0.0)
|
|
67
|
-
if zero_scores > 0
|
|
68
|
-
penalty_factor = 1.0 - (zero_scores.to_f / scores.length * 0.5) # Up to 50% penalty
|
|
69
|
-
base_score *= penalty_factor
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
base_score
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Calculate score for any field value against a filter.
|
|
76
|
-
# Returns a score between 0.0 and 1.0 based on how well the value matches the filter.
|
|
77
|
-
#
|
|
78
|
-
# @param value [Object] The value to match against the filter.
|
|
79
|
-
# @param filter [String, Regexp, Object] The filter to match the value against.
|
|
80
|
-
# @return [Float] A score between 0.0 and 1.0 indicating match quality.
|
|
81
|
-
def calculate_field_score(value, filter)
|
|
82
|
-
return 0.0 unless value && filter
|
|
83
|
-
|
|
84
|
-
case filter
|
|
85
|
-
when String
|
|
86
|
-
value_str = value.to_s
|
|
87
|
-
if value_str == filter
|
|
88
|
-
1.0
|
|
89
|
-
elsif value_str.include?(filter)
|
|
90
|
-
0.7
|
|
91
|
-
else
|
|
92
|
-
# Use string similarity for partial matching
|
|
93
|
-
similarity = string_similarity(value_str, filter)
|
|
94
|
-
(similarity > 0.5) ? similarity * 0.6 : 0.0
|
|
95
|
-
end
|
|
96
|
-
when Regexp
|
|
97
|
-
filter.match?(value.to_s) ? 1.0 : 0.0
|
|
98
|
-
else
|
|
99
|
-
# For other matchers (like RSpec matchers), try to use === operator
|
|
100
|
-
begin
|
|
101
|
-
(filter === value) ? 1.0 : 0.0
|
|
102
|
-
rescue
|
|
103
|
-
0.0
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Calculate proximity score based on log severity distance.
|
|
109
|
-
# Provides partial scoring for severities that are close to the target.
|
|
110
|
-
#
|
|
111
|
-
# @param entry_severity [Integer] The severity level of the log entry.
|
|
112
|
-
# @param filter_severity [Integer] The target severity level to match.
|
|
113
|
-
# @return [Float] A score between 0.0 and 1.0 based on severity proximity.
|
|
114
|
-
def severity_proximity_score(entry_severity, filter_severity)
|
|
115
|
-
severity_diff = (entry_severity - filter_severity).abs
|
|
116
|
-
case severity_diff
|
|
117
|
-
when 0 then 1.0
|
|
118
|
-
when 1 then 0.7
|
|
119
|
-
when 2 then 0.4
|
|
120
|
-
else 0.0
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Calculate score for attribute matching.
|
|
125
|
-
# Compares entry attributes against filter attributes and returns a score
|
|
126
|
-
# based on how many attributes match.
|
|
127
|
-
#
|
|
128
|
-
# @param entry_attributes [Hash] The attributes from the log entry.
|
|
129
|
-
# @param attributes_filter [Hash] The attributes filter to match against.
|
|
130
|
-
# @return [Float] A score between 0.0 and 1.0 based on attribute matches.
|
|
131
|
-
def calculate_attributes_score(entry_attributes, attributes_filter)
|
|
132
|
-
return 0.0 unless entry_attributes && attributes_filter.is_a?(Hash)
|
|
133
|
-
|
|
134
|
-
attributes_filter = deep_stringify_keys(Lumberjack::Utils.expand_attributes(attributes_filter))
|
|
135
|
-
attributes = deep_stringify_keys(Lumberjack::Utils.expand_attributes(entry_attributes))
|
|
136
|
-
|
|
137
|
-
total_attribute_filters = count_attribute_filters(attributes_filter)
|
|
138
|
-
return 0.0 if total_attribute_filters == 0
|
|
139
|
-
|
|
140
|
-
matched_attributes = count_matched_attributes(attributes, attributes_filter)
|
|
141
|
-
matched_attributes.to_f / total_attribute_filters
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
private
|
|
145
|
-
|
|
146
|
-
# Calculate string similarity using a simple Levenshtein distance-based approach.
|
|
147
|
-
# Returns a score between 0.0 and 1.0 where 1.0 is an exact match.
|
|
148
|
-
#
|
|
149
|
-
# @param str1 [String] The first string to compare.
|
|
150
|
-
# @param str2 [String] The second string to compare.
|
|
151
|
-
# @return [Float] A similarity score between 0.0 and 1.0.
|
|
152
|
-
def string_similarity(str1, str2)
|
|
153
|
-
return 1.0 if str1 == str2
|
|
154
|
-
return 0.0 if str1.nil? || str2.nil? || str1.empty? || str2.empty?
|
|
155
|
-
|
|
156
|
-
# Convert to lowercase for case-insensitive comparison
|
|
157
|
-
s1 = str1.downcase
|
|
158
|
-
s2 = str2.downcase
|
|
159
|
-
|
|
160
|
-
# If one string contains the other, give it a good score
|
|
161
|
-
if s1.include?(s2) || s2.include?(s1)
|
|
162
|
-
shorter = [s1.length, s2.length].min
|
|
163
|
-
longer = [s1.length, s2.length].max
|
|
164
|
-
return shorter.to_f / longer * 0.8 + 0.2 # Boost score for containment
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Calculate Levenshtein distance
|
|
168
|
-
distance = levenshtein_distance(s1, s2)
|
|
169
|
-
max_length = [s1.length, s2.length].max
|
|
170
|
-
|
|
171
|
-
# Convert distance to similarity score
|
|
172
|
-
return 0.0 if max_length == 0
|
|
173
|
-
|
|
174
|
-
1.0 - (distance.to_f / max_length)
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
# Simple Levenshtein distance implementation.
|
|
178
|
-
# Calculates the minimum number of single-character edits needed
|
|
179
|
-
# to change one string into another.
|
|
180
|
-
#
|
|
181
|
-
# @param str1 [String] The first string.
|
|
182
|
-
# @param str2 [String] The second string.
|
|
183
|
-
# @return [Integer] The Levenshtein distance between the strings.
|
|
184
|
-
def levenshtein_distance(str1, str2)
|
|
185
|
-
return str2.length if str1.empty?
|
|
186
|
-
return str1.length if str2.empty?
|
|
187
|
-
|
|
188
|
-
matrix = Array.new(str1.length + 1) { Array.new(str2.length + 1, 0) }
|
|
189
|
-
|
|
190
|
-
# Initialize first row and column
|
|
191
|
-
(0..str1.length).each { |i| matrix[i][0] = i }
|
|
192
|
-
(0..str2.length).each { |j| matrix[0][j] = j }
|
|
193
|
-
|
|
194
|
-
# Fill the matrix
|
|
195
|
-
(1..str1.length).each do |i|
|
|
196
|
-
(1..str2.length).each do |j|
|
|
197
|
-
cost = (str1[i - 1] == str2[j - 1]) ? 0 : 1
|
|
198
|
-
matrix[i][j] = [
|
|
199
|
-
matrix[i - 1][j] + 1, # deletion
|
|
200
|
-
matrix[i][j - 1] + 1, # insertion
|
|
201
|
-
matrix[i - 1][j - 1] + cost # substitution
|
|
202
|
-
].min
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
matrix[str1.length][str2.length]
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# Count the total number of attribute filters in a nested hash structure.
|
|
210
|
-
#
|
|
211
|
-
# @param attributes_filter [Hash] The attributes filter hash to count.
|
|
212
|
-
# @param count [Integer] The current count (used for recursion).
|
|
213
|
-
# @return [Integer] The total number of filters.
|
|
214
|
-
def count_attribute_filters(attributes_filter, count = 0)
|
|
215
|
-
attributes_filter.each do |_name, value_filter|
|
|
216
|
-
if value_filter.is_a?(Hash)
|
|
217
|
-
count = count_attribute_filters(value_filter, count)
|
|
218
|
-
else
|
|
219
|
-
count += 1
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
count
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
# Count the number of matched attributes in a nested structure.
|
|
226
|
-
#
|
|
227
|
-
# @param attributes [Hash] The log entry attributes to check.
|
|
228
|
-
# @param attributes_filter [Hash] The filter attributes to match against.
|
|
229
|
-
# @param count [Integer] The current count (used for recursion).
|
|
230
|
-
# @return [Integer] The number of matched attributes.
|
|
231
|
-
def count_matched_attributes(attributes, attributes_filter, count = 0)
|
|
232
|
-
return count unless attributes && attributes_filter
|
|
233
|
-
|
|
234
|
-
attributes_filter.each do |name, value_filter|
|
|
235
|
-
name = name.to_s
|
|
236
|
-
attribute_values = attributes[name]
|
|
237
|
-
|
|
238
|
-
if value_filter.is_a?(Hash) && attribute_values.is_a?(Hash)
|
|
239
|
-
count = count_matched_attributes(attribute_values, value_filter, count)
|
|
240
|
-
elsif attributes.include?(name) && exact_match?(attribute_values, value_filter)
|
|
241
|
-
count += 1
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
count
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
# Check if a value exactly matches the filter using the === operator.
|
|
248
|
-
#
|
|
249
|
-
# @param value [Object] The value to match.
|
|
250
|
-
# @param filter [Object] The filter to match against.
|
|
251
|
-
# @return [Boolean] True if the value matches the filter.
|
|
252
|
-
def exact_match?(value, filter)
|
|
253
|
-
return true unless filter
|
|
254
|
-
|
|
255
|
-
filter === value
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
# Recursively convert all keys in a hash structure to strings.
|
|
259
|
-
#
|
|
260
|
-
# @param hash [Hash, Object] The hash to stringify or other object to return as-is.
|
|
261
|
-
# @return [Hash, Object] The hash with string keys or the original object.
|
|
262
|
-
def deep_stringify_keys(hash)
|
|
263
|
-
if hash.is_a?(Hash)
|
|
264
|
-
hash.each_with_object({}) do |(key, value), result|
|
|
265
|
-
new_key = key.to_s
|
|
266
|
-
new_value = deep_stringify_keys(value)
|
|
267
|
-
result[new_key] = new_value
|
|
268
|
-
end
|
|
269
|
-
elsif hash.is_a?(Enumerable)
|
|
270
|
-
hash.collect { |item| deep_stringify_keys(item) }
|
|
271
|
-
else
|
|
272
|
-
hash
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
end
|