lumberjack_capture_device 1.2.1 → 2.0.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 +26 -0
- data/README.md +26 -6
- data/VERSION +1 -1
- data/lib/lumberjack/capture_device/entry_score.rb +98 -42
- data/lib/lumberjack/capture_device/include_log_entry_matcher.rb +106 -34
- data/lib/lumberjack/capture_device/rspec.rb +59 -2
- data/lib/lumberjack/capture_device.rb +158 -171
- data/lumberjack_capture_device.gemspec +2 -2
- metadata +6 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be90765c9b26b0ca9c501504e83c2a58cf1f2ce74edffe0b5e3d8e2515194277
|
|
4
|
+
data.tar.gz: '0825d9fa6621df4c5cdea31de794e1c8f263249d3f42f0e17945806ccb7f5f69'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5423feff017c2b44974d7e75268c39388353165128632d0b7533619b5271ff17aeb1e9250d7ceda017de56f272baf0bda9b6e2a180a98397b13520b128f5449b
|
|
7
|
+
data.tar.gz: 3bbdb6b6ca43683a3ec20ead94fa93e557b33194c4ee58d5f36c478e50b01e7ff5abed29a3001fce885f47f304f62d3bcbeec5cddd00708401f377db7d775bf1
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ 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.0.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Captured log entries are now passed through to the underlying logging device when the capture block is finished. This can be disabled by passing `write_to_original: false` to the `capture` method. You can then write the captured entries to the underlying device manually by calling `write_to_underlying_device`.
|
|
12
|
+
- Added `capture_logger_around_example` RSpec helper method to simplify capturing log entries in an `around` hook.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Depends on `lumberjack` 2.0 or greater.
|
|
17
|
+
|
|
18
|
+
### Deprecated
|
|
19
|
+
|
|
20
|
+
- The `:level` and `:tags` options on the matching methods (`include?`, `match`, `closest_match`, and `extract`) has been deprecated in favor of `:severity` and `:attributes`.
|
|
21
|
+
|
|
22
|
+
## 1.2.2
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Improved failure message on RSpec matcher.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `Lumberjack::CaptureDevice` now acts as an enumerable object.
|
|
31
|
+
- Exposed helper methods for formatting log entries.
|
|
32
|
+
|
|
7
33
|
## 1.2.1
|
|
8
34
|
|
|
9
35
|
### Changed
|
data/README.md
CHANGED
|
@@ -44,17 +44,17 @@ logs = Lumberjack::CaptureDevice.capture(Rails.logger) { do_something }
|
|
|
44
44
|
assert(logs.include?(level: :info, message: "Something happened"))
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
You can filter the logs on level, message, and
|
|
47
|
+
You can filter the logs on level, message, and attributes.
|
|
48
48
|
|
|
49
49
|
- The level 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
|
-
- The
|
|
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
54
|
expect(logs).to include(level: :info, message: /something/i)
|
|
55
|
-
expect(logs).to include(level: Logger::INFO,
|
|
56
|
-
expect(logs).to include(
|
|
57
|
-
expect(logs).to include(
|
|
55
|
+
expect(logs).to include(level: Logger::INFO, attributes: {foo: "bar"})
|
|
56
|
+
expect(logs).to include(attributes: {foo: anything, count: {one: 1}})
|
|
57
|
+
expect(logs).to include(attributes: {foo: anything, "count.one" => 1})
|
|
58
58
|
```
|
|
59
59
|
|
|
60
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#buffer`.
|
|
@@ -72,12 +72,32 @@ This will give you a `capture_logger` method and `include_log_entry` matcher. Th
|
|
|
72
72
|
```ruby
|
|
73
73
|
describe MyClass do
|
|
74
74
|
it "logs information" do
|
|
75
|
-
logs = capture_logger { MyClass.do_something }
|
|
75
|
+
logs = capture_logger(Rails.logger) { MyClass.do_something }
|
|
76
76
|
expect(logs).to include_log_entry(message: "Something")
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
You can also set up log capturing around each example with the `capture_logger_around_example` method.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
describe MyClass do
|
|
85
|
+
around do |example|
|
|
86
|
+
capture_logger_around_example(Rails.logger, example)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "logs information" do
|
|
90
|
+
MyClass.do_something
|
|
91
|
+
expect(Rails.logger).to include_log_entry(message: "Something")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> [!TIP]
|
|
97
|
+
> Add `capture_logger_around_example` as a global `around` hook in your RSpec configuration to automatically capture log entries for every example.
|
|
98
|
+
>
|
|
99
|
+
> This will also suppress all log output during tests unless an example fails which can reduce noise in the logs from tests that don't fail. This is especially useful in CI environments where you can save the logs as an artifact for failed test runs.
|
|
100
|
+
|
|
81
101
|
## Installation
|
|
82
102
|
|
|
83
103
|
Add this line to your application's Gemfile:
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2.0.0
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Class responsible for scoring and matching log entries against filters
|
|
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.
|
|
4
6
|
class Lumberjack::CaptureDevice::EntryScore
|
|
5
7
|
# Minimum score threshold for considering a match (30% match)
|
|
6
8
|
MIN_SCORE_THRESHOLD = 0.3
|
|
7
9
|
|
|
8
10
|
class << self
|
|
9
|
-
# Calculate the overall match score for an entry against all provided filters
|
|
10
|
-
# Returns a score between 0.0 and 1.0
|
|
11
|
-
|
|
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)
|
|
12
21
|
scores = []
|
|
13
22
|
weights = []
|
|
14
23
|
|
|
@@ -19,14 +28,14 @@ class Lumberjack::CaptureDevice::EntryScore
|
|
|
19
28
|
weights << 0.4 # Weight message matching highly
|
|
20
29
|
end
|
|
21
30
|
|
|
22
|
-
# Check
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
1.0 # Exact
|
|
31
|
+
# Check severity match
|
|
32
|
+
if severity_filter
|
|
33
|
+
severity_score = if entry.severity == severity_filter
|
|
34
|
+
1.0 # Exact severity match
|
|
26
35
|
else
|
|
27
|
-
|
|
36
|
+
severity_proximity_score(entry.severity, severity_filter) # Partial severity match
|
|
28
37
|
end
|
|
29
|
-
scores <<
|
|
38
|
+
scores << severity_score
|
|
30
39
|
weights << 0.3
|
|
31
40
|
end
|
|
32
41
|
|
|
@@ -37,10 +46,10 @@ class Lumberjack::CaptureDevice::EntryScore
|
|
|
37
46
|
weights << 0.2
|
|
38
47
|
end
|
|
39
48
|
|
|
40
|
-
# Check
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
-
scores <<
|
|
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
|
|
44
53
|
weights << 0.3
|
|
45
54
|
end
|
|
46
55
|
|
|
@@ -63,8 +72,12 @@ class Lumberjack::CaptureDevice::EntryScore
|
|
|
63
72
|
base_score
|
|
64
73
|
end
|
|
65
74
|
|
|
66
|
-
# Calculate score for any field value against a filter
|
|
67
|
-
# Returns a score between 0.0 and 1.0 based on how well the value matches the filter
|
|
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.
|
|
68
81
|
def calculate_field_score(value, filter)
|
|
69
82
|
return 0.0 unless value && filter
|
|
70
83
|
|
|
@@ -92,10 +105,15 @@ class Lumberjack::CaptureDevice::EntryScore
|
|
|
92
105
|
end
|
|
93
106
|
end
|
|
94
107
|
|
|
95
|
-
# Calculate proximity score based on log
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
99
117
|
when 0 then 1.0
|
|
100
118
|
when 1 then 0.7
|
|
101
119
|
when 2 then 0.4
|
|
@@ -103,24 +121,34 @@ class Lumberjack::CaptureDevice::EntryScore
|
|
|
103
121
|
end
|
|
104
122
|
end
|
|
105
123
|
|
|
106
|
-
# Calculate score for
|
|
107
|
-
|
|
108
|
-
|
|
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)
|
|
109
133
|
|
|
110
|
-
|
|
111
|
-
|
|
134
|
+
attributes_filter = deep_stringify_keys(Lumberjack::Utils.expand_attributes(attributes_filter))
|
|
135
|
+
attributes = deep_stringify_keys(Lumberjack::Utils.expand_attributes(entry_attributes))
|
|
112
136
|
|
|
113
|
-
|
|
114
|
-
return 0.0 if
|
|
137
|
+
total_attribute_filters = count_attribute_filters(attributes_filter)
|
|
138
|
+
return 0.0 if total_attribute_filters == 0
|
|
115
139
|
|
|
116
|
-
|
|
117
|
-
|
|
140
|
+
matched_attributes = count_matched_attributes(attributes, attributes_filter)
|
|
141
|
+
matched_attributes.to_f / total_attribute_filters
|
|
118
142
|
end
|
|
119
143
|
|
|
120
144
|
private
|
|
121
145
|
|
|
122
|
-
# Calculate string similarity using a simple Levenshtein distance-based approach
|
|
123
|
-
# Returns a score between 0.0 and 1.0 where 1.0 is an exact match
|
|
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.
|
|
124
152
|
def string_similarity(str1, str2)
|
|
125
153
|
return 1.0 if str1 == str2
|
|
126
154
|
return 0.0 if str1.nil? || str2.nil? || str1.empty? || str2.empty?
|
|
@@ -142,10 +170,17 @@ class Lumberjack::CaptureDevice::EntryScore
|
|
|
142
170
|
|
|
143
171
|
# Convert distance to similarity score
|
|
144
172
|
return 0.0 if max_length == 0
|
|
173
|
+
|
|
145
174
|
1.0 - (distance.to_f / max_length)
|
|
146
175
|
end
|
|
147
176
|
|
|
148
|
-
# Simple Levenshtein distance implementation
|
|
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.
|
|
149
184
|
def levenshtein_distance(str1, str2)
|
|
150
185
|
return str2.length if str1.empty?
|
|
151
186
|
return str1.length if str2.empty?
|
|
@@ -171,10 +206,15 @@ class Lumberjack::CaptureDevice::EntryScore
|
|
|
171
206
|
matrix[str1.length][str2.length]
|
|
172
207
|
end
|
|
173
208
|
|
|
174
|
-
|
|
175
|
-
|
|
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|
|
|
176
216
|
if value_filter.is_a?(Hash)
|
|
177
|
-
count =
|
|
217
|
+
count = count_attribute_filters(value_filter, count)
|
|
178
218
|
else
|
|
179
219
|
count += 1
|
|
180
220
|
end
|
|
@@ -182,27 +222,43 @@ class Lumberjack::CaptureDevice::EntryScore
|
|
|
182
222
|
count
|
|
183
223
|
end
|
|
184
224
|
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
187
233
|
|
|
188
|
-
|
|
234
|
+
attributes_filter.each do |name, value_filter|
|
|
189
235
|
name = name.to_s
|
|
190
|
-
|
|
236
|
+
attribute_values = attributes[name]
|
|
191
237
|
|
|
192
|
-
if value_filter.is_a?(Hash) &&
|
|
193
|
-
count =
|
|
194
|
-
elsif
|
|
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)
|
|
195
241
|
count += 1
|
|
196
242
|
end
|
|
197
243
|
end
|
|
198
244
|
count
|
|
199
245
|
end
|
|
200
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.
|
|
201
252
|
def exact_match?(value, filter)
|
|
202
253
|
return true unless filter
|
|
254
|
+
|
|
203
255
|
filter === value
|
|
204
256
|
end
|
|
205
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.
|
|
206
262
|
def deep_stringify_keys(hash)
|
|
207
263
|
if hash.is_a?(Hash)
|
|
208
264
|
hash.each_with_object({}) do |(key, value), result|
|
|
@@ -2,79 +2,151 @@
|
|
|
2
2
|
|
|
3
3
|
# RSpec matcher for checking captured logs for specific entries.
|
|
4
4
|
class Lumberjack::CaptureDevice::IncludeLogEntryMatcher
|
|
5
|
+
# Initialize the matcher with expected log entry attributes.
|
|
6
|
+
#
|
|
7
|
+
# @param expected_hash [Hash] Expected log entry attributes to match against.
|
|
5
8
|
def initialize(expected_hash)
|
|
6
9
|
@expected_hash = expected_hash.transform_keys(&:to_sym)
|
|
7
|
-
@
|
|
10
|
+
@logger = nil
|
|
8
11
|
end
|
|
9
12
|
|
|
13
|
+
# Check if the logger contains a log entry matching the expected attributes.
|
|
14
|
+
#
|
|
15
|
+
# @param actual [Lumberjack::Logger, Lumberjack::ForkedLogger] The logger to check. The logger must be using
|
|
16
|
+
# a Lumberjack::Device::Test device.
|
|
17
|
+
# @return [Boolean] True if a matching log entry is found.
|
|
10
18
|
def matches?(actual)
|
|
11
|
-
@
|
|
12
|
-
return false unless
|
|
19
|
+
@logger = actual
|
|
20
|
+
return false unless valid_logger?
|
|
13
21
|
|
|
14
|
-
@
|
|
22
|
+
device = @logger.is_a?(Lumberjack::Device::Test) ? @logger : @logger.device
|
|
23
|
+
device.include?(@expected_hash)
|
|
15
24
|
end
|
|
16
25
|
|
|
26
|
+
# Generate a failure message when the matcher fails.
|
|
27
|
+
#
|
|
28
|
+
# @return [String] A formatted failure message.
|
|
17
29
|
def failure_message
|
|
18
|
-
if
|
|
19
|
-
formatted_failure_message(@
|
|
30
|
+
if valid_logger?
|
|
31
|
+
formatted_failure_message(@logger, @expected_hash)
|
|
20
32
|
else
|
|
21
|
-
wrong_object_type_message(@
|
|
33
|
+
wrong_object_type_message(@logger)
|
|
22
34
|
end
|
|
23
35
|
end
|
|
24
36
|
|
|
37
|
+
# Generate a failure message when the negated matcher fails.
|
|
38
|
+
#
|
|
39
|
+
# @return [String] A formatted failure message for negated expectations.
|
|
25
40
|
def failure_message_when_negated
|
|
26
|
-
if
|
|
27
|
-
|
|
41
|
+
if valid_logger?
|
|
42
|
+
formatted_negated_failure_message(@logger, @expected_hash)
|
|
28
43
|
else
|
|
29
|
-
wrong_object_type_message(@
|
|
44
|
+
wrong_object_type_message(@logger)
|
|
30
45
|
end
|
|
31
46
|
end
|
|
32
47
|
|
|
48
|
+
# Provide a description of what this matcher checks.
|
|
49
|
+
#
|
|
50
|
+
# @return [String] A human-readable description of the matcher.
|
|
33
51
|
def description
|
|
34
52
|
"have logged entry with #{expectation_description(@expected_hash)}"
|
|
35
53
|
end
|
|
36
54
|
|
|
37
55
|
private
|
|
38
56
|
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
# Check if the logger is using a valid Lumberjack::Device::Test device.
|
|
58
|
+
#
|
|
59
|
+
# @return [Boolean] True if the logger is a Lumberjack::Device::Test.
|
|
60
|
+
def valid_logger?
|
|
61
|
+
return true if @logger.is_a?(Lumberjack::Device::Test)
|
|
62
|
+
return false unless @logger.respond_to?(:device)
|
|
63
|
+
|
|
64
|
+
@logger.device.is_a?(Lumberjack::Device::Test)
|
|
41
65
|
end
|
|
42
66
|
|
|
43
|
-
|
|
44
|
-
|
|
67
|
+
# Generate an error message for wrong object type.
|
|
68
|
+
#
|
|
69
|
+
# @param logger [Object] The object that was passed instead of a Lumberjack::Device::Test.
|
|
70
|
+
# @return [String] An error message describing the type mismatch.
|
|
71
|
+
def wrong_object_type_message(logger)
|
|
72
|
+
unless logger.respond_to?(:device)
|
|
73
|
+
return "Expected a Lumberjack::Logger object, but received a #{logger.class}."
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
device = logger.device
|
|
77
|
+
"Expected logger device to be a Lumberjack::Device::Test, but it is a #{device.class}."
|
|
45
78
|
end
|
|
46
79
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
80
|
+
# Generate a detailed failure message showing expected vs actual logs.
|
|
81
|
+
#
|
|
82
|
+
# @param logger_or_device [Lumberjack::Device::Test] The logger device.
|
|
83
|
+
# @param expected_hash [Hash] The expected log entry attributes.
|
|
84
|
+
# @return [String] A formatted failure message with context.
|
|
85
|
+
def formatted_failure_message(logger_or_device, expected_hash)
|
|
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
|
|
52
95
|
|
|
96
|
+
message = +"expected logs to include entry:\n" \
|
|
97
|
+
"#{Lumberjack::Device::Test.formatted_expectation(expected_hash, indent: 2)}"
|
|
98
|
+
|
|
99
|
+
closest_match = device.closest_match(**expected_hash)
|
|
53
100
|
if closest_match
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
101
|
+
message << "\n\nClosest match found:" \
|
|
102
|
+
"#{Lumberjack::Device::Test.formatted_expectation(closest_match, indent: 2)}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
entries = device.entries
|
|
106
|
+
message << "\n\nLogged #{entries.length} #{(entries.length == 1) ? "entry" : "entries"}"
|
|
107
|
+
if entries.length > 0
|
|
108
|
+
message << "\n----------------------\n"
|
|
109
|
+
template = Lumberjack::LocalLogTemplate.new
|
|
110
|
+
entries.each do |entry|
|
|
111
|
+
message << "#{template.call(entry)}\n"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
message
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Generate a failure message for negated expectations.
|
|
119
|
+
#
|
|
120
|
+
# @param logger_or_device [Lumberjack::Device::Test] The logger to check.
|
|
121
|
+
# @param expected_hash [Hash] The expected log entry attributes that should not be present.
|
|
122
|
+
# @return [String] A formatted failure message for negated expectations.
|
|
123
|
+
def formatted_negated_failure_message(logger_or_device, expected_hash)
|
|
124
|
+
device = logger_or_device.respond_to?(:device) ? logger_or_device.device : logger_or_device
|
|
125
|
+
message = "expected logs not to include entry:\n" \
|
|
126
|
+
"#{Lumberjack::Device::Test.formatted_expectation(expected_hash, indent: 2)}"
|
|
127
|
+
|
|
128
|
+
match = device.match(**expected_hash)
|
|
129
|
+
if match
|
|
130
|
+
message = "#{message}\n\nFound entry:\n" \
|
|
131
|
+
"#{Lumberjack::Device::Test.formatted_expectation(match, indent: 2)}"
|
|
64
132
|
end
|
|
65
133
|
|
|
66
134
|
message
|
|
67
135
|
end
|
|
68
136
|
|
|
137
|
+
# Create a human-readable description of the expected log entry attributes.
|
|
138
|
+
#
|
|
139
|
+
# @param expected_hash [Hash] The expected log entry attributes.
|
|
140
|
+
# @return [String] A formatted description of the expected attributes.
|
|
69
141
|
def expectation_description(expected_hash)
|
|
70
142
|
info = []
|
|
71
|
-
info << "
|
|
143
|
+
info << "severity: #{expected_hash[:severity].inspect}" unless expected_hash[:severity].nil?
|
|
72
144
|
info << "message: #{expected_hash[:message].inspect}" unless expected_hash[:message].nil?
|
|
73
145
|
info << "progname: #{expected_hash[:progname].inspect}" unless expected_hash[:progname].nil?
|
|
74
|
-
if expected_hash[:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
info << "
|
|
146
|
+
if expected_hash[:attributes].is_a?(Hash) && !expected_hash[:attributes].empty?
|
|
147
|
+
attributes = Lumberjack::Utils.flatten_attributes(expected_hash[:attributes])
|
|
148
|
+
attributes_info = attributes.collect { |name, value| "#{name}=#{value.inspect}" }.join(", ")
|
|
149
|
+
info << "attributes: #{attributes_info}"
|
|
78
150
|
end
|
|
79
151
|
info.join(", ")
|
|
80
152
|
end
|
|
@@ -3,13 +3,70 @@
|
|
|
3
3
|
require_relative "../capture_device"
|
|
4
4
|
require "rspec"
|
|
5
5
|
|
|
6
|
+
# RSpec helper methods for working with CaptureDevice.
|
|
6
7
|
module Lumberjack::CaptureDevice::RSpec
|
|
8
|
+
# Create a matcher for checking if a log entry is included in the captured logs.
|
|
9
|
+
# This matcher provides better error messages than using the include? method directly.
|
|
10
|
+
#
|
|
11
|
+
# @param expected_hash [Hash] The expected log entry attributes to match.
|
|
12
|
+
# @option expected_hash [String, Symbol, Integer] :level The expected log level.
|
|
13
|
+
# @option expected_hash [String, Symbol, Integer] :severity Alias for :level.
|
|
14
|
+
# @option expected_hash [String, Regexp] :message The expected message content.
|
|
15
|
+
# @option expected_hash [Hash] :attributes Expected log entry attributes.
|
|
16
|
+
# @option expected_hash [Hash] :tags Alias for :attributes.
|
|
17
|
+
# @option expected_hash [String] :progname Expected program name.
|
|
18
|
+
# @return [Lumberjack::CaptureDevice::IncludeLogEntryMatcher] A matcher for the expected log entry.
|
|
19
|
+
# @example
|
|
20
|
+
# expect(logs).to include_log_entry(level: :info, message: "User logged in")
|
|
21
|
+
# @example
|
|
22
|
+
# expect(logs).to include_log_entry(message: /error/i, attributes: {user_id: 123})
|
|
7
23
|
def include_log_entry(expected_hash)
|
|
8
24
|
Lumberjack::CaptureDevice::IncludeLogEntryMatcher.new(expected_hash)
|
|
9
25
|
end
|
|
10
26
|
|
|
11
|
-
|
|
12
|
-
|
|
27
|
+
# Capture log entries from a logger within a block. This method temporarily
|
|
28
|
+
# replaces the logger's device with a CaptureDevice, sets the log level to debug,
|
|
29
|
+
# and removes formatters to capture raw log entries for testing.
|
|
30
|
+
#
|
|
31
|
+
# @param logger [Lumberjack::Logger] The logger to capture entries from.
|
|
32
|
+
# @yield [device] The block to execute while capturing log entries.
|
|
33
|
+
# @yieldparam device [Lumberjack::CaptureDevice] The device that will capture the log entries.
|
|
34
|
+
# @return [Lumberjack::CaptureDevice] The device that captured the log entries.
|
|
35
|
+
# @example
|
|
36
|
+
# logs = capture_logger(Rails.logger) do
|
|
37
|
+
# Rails.logger.info("Test message")
|
|
38
|
+
# end
|
|
39
|
+
# expect(logs).to include_log_entry(level: :info, message: "Test message")
|
|
40
|
+
def capture_logger(logger, write_to_original: true, &block)
|
|
41
|
+
Lumberjack::CaptureDevice.capture(logger, write_to_original: write_to_original, &block)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# RSpec around hook to automatically capture logs for each example. The captured logs are only
|
|
45
|
+
# written to the original logger if the example fails. This helps keep the logs more usable for
|
|
46
|
+
# debugging test failures since it removes all the noise from passing tests.
|
|
47
|
+
#
|
|
48
|
+
# This is designed for CI environments where you can save the logs as artifacts of the test run.
|
|
49
|
+
#
|
|
50
|
+
# @param logger [Lumberjack::Logger] The logger to capture entries for.
|
|
51
|
+
# @param example [RSpec::Core::Example] The current RSpec example.
|
|
52
|
+
#
|
|
53
|
+
# @example Capture logs for a Rails application
|
|
54
|
+
# # In your spec_helper.rb or rails_helper.rb
|
|
55
|
+
# RSpec.configure do |config|
|
|
56
|
+
# config.around do |example|
|
|
57
|
+
# capture_logger_around_example(Rails.logger, example)
|
|
58
|
+
# end
|
|
59
|
+
# end
|
|
60
|
+
def capture_logger_around_example(logger, example)
|
|
61
|
+
capture_logger(logger, write_to_original: false) do |captured_device|
|
|
62
|
+
example.run
|
|
63
|
+
|
|
64
|
+
if example.exception
|
|
65
|
+
logger.tag(rspec: {source_location: example.source_location, description: example.metadata[:description]}) do
|
|
66
|
+
captured_device.write_to_underlying_device
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
13
70
|
end
|
|
14
71
|
end
|
|
15
72
|
|
|
@@ -5,10 +5,12 @@ require "lumberjack"
|
|
|
5
5
|
module Lumberjack
|
|
6
6
|
# Lumberjack device for capturing log entries into memory to allow them to be inspected
|
|
7
7
|
# for testing purposes.
|
|
8
|
-
class CaptureDevice < Lumberjack::Device
|
|
9
|
-
VERSION = File.read(File.join(__dir__, "..", "..", "VERSION"))
|
|
8
|
+
class CaptureDevice < Lumberjack::Device::Test
|
|
9
|
+
VERSION = ::File.read(::File.join(__dir__, "..", "..", "VERSION")).strip.freeze
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
require_relative "capture_device/include_log_entry_matcher"
|
|
12
|
+
|
|
13
|
+
include Enumerable
|
|
12
14
|
|
|
13
15
|
class << self
|
|
14
16
|
# Capture the entries written by the logger within a block. Within the block all log
|
|
@@ -18,125 +20,153 @@ module Lumberjack
|
|
|
18
20
|
# by the method call.
|
|
19
21
|
#
|
|
20
22
|
# @param logger [Lumberjack::Logger] The logger to capture entries from.
|
|
23
|
+
# @param write_to_original [Boolean] If true (the default) the captured entries will be written
|
|
24
|
+
# back to the original device when the block completes. If false, the captured entries
|
|
25
|
+
# will not be written back.
|
|
21
26
|
# @yield [device] The block to execute while capturing log entries.
|
|
22
27
|
# @return [Lumberjack::CaptureDevice] The device that captured the log entries.
|
|
23
28
|
# @yieldparam device [Lumberjack::CaptureDevice] The device that will capture the log entries.
|
|
24
29
|
# @example
|
|
25
30
|
# Lumberjack::CaptureDevice.capture(logger) do |logs|
|
|
26
31
|
# logger.info("This will be captured")
|
|
27
|
-
# expect(logs).to include(
|
|
32
|
+
# expect(logs).to include(severity: :info, message: "This will be captured")
|
|
28
33
|
# end
|
|
29
34
|
#
|
|
30
35
|
# @example
|
|
31
36
|
# logs = Lumberjack::CaptureDevice.capture(logger) { logger.info("This will be captured") }
|
|
32
|
-
# expect(logs).to include(
|
|
33
|
-
def capture(logger)
|
|
34
|
-
device = new
|
|
37
|
+
# expect(logs).to include(severity: :info, message: "This will be captured")
|
|
38
|
+
def capture(logger, write_to_original: true)
|
|
35
39
|
save_device = logger.device
|
|
36
40
|
save_level = logger.level
|
|
37
|
-
|
|
41
|
+
device = new(underlying_device: save_device)
|
|
42
|
+
|
|
38
43
|
begin
|
|
39
44
|
logger.device = device
|
|
40
45
|
logger.level = :debug
|
|
41
|
-
logger.formatter = Lumberjack::Formatter.empty
|
|
42
46
|
yield device
|
|
43
47
|
ensure
|
|
44
48
|
logger.device = save_device
|
|
45
49
|
logger.level = save_level
|
|
46
|
-
|
|
50
|
+
device.write_to_underlying_device if write_to_original
|
|
47
51
|
end
|
|
48
|
-
device
|
|
49
|
-
end
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
expectation = expectation.transform_keys(&:to_s).compact
|
|
53
|
-
message = []
|
|
54
|
-
message << "level: #{expectation["level"].inspect}" if expectation.include?("level")
|
|
55
|
-
message << "message: #{expectation["message"].inspect}" if expectation.include?("message")
|
|
56
|
-
message << "progname: #{expectation["progname"].inspect}" if expectation.include?("progname")
|
|
57
|
-
if expectation["tags"].is_a?(Hash) && !expectation["tags"].empty?
|
|
58
|
-
tags = Lumberjack::Utils.flatten_tags(expectation["tags"])
|
|
59
|
-
prefix = "tags: "
|
|
60
|
-
tags.each do |name, value|
|
|
61
|
-
message << "#{prefix} #{name}: #{value.inspect}"
|
|
62
|
-
prefix = " "
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
message.join("\n")
|
|
53
|
+
device
|
|
66
54
|
end
|
|
67
55
|
end
|
|
68
56
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
57
|
+
# The original device from the logger before capture started.
|
|
58
|
+
#
|
|
59
|
+
# @return [Lumberjack::Device, nil] The original device, or nil if none was set.
|
|
60
|
+
attr_reader :underlying_device
|
|
72
61
|
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
# Initialize a new CaptureDevice.
|
|
63
|
+
#
|
|
64
|
+
# @param options [Hash] Options to pass to the parent Test device.
|
|
65
|
+
def initialize(options = {})
|
|
66
|
+
@underlying_device = options[:underlying_device]
|
|
67
|
+
super(options.merge(max_entries: 1_000_000))
|
|
75
68
|
end
|
|
76
69
|
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
70
|
+
# Return all the captured entries that match the specified filters. These filters are
|
|
71
|
+
# the same as described in the `include?` method.
|
|
72
|
+
#
|
|
73
|
+
# @param message [String, Regexp, nil] The message to match against the log entries.
|
|
74
|
+
# @param severity [String, Symbol, Integer, nil] The severity to match against the log entries.
|
|
75
|
+
# @param attributes [Hash, nil] A hash of attribute names to values to match against the log entries. The attributes
|
|
76
|
+
# will match nested attributes using dot notation (e.g. `foo.bar` will match an attribute with the structure
|
|
77
|
+
# +{foo: {bar: "value"}}+).
|
|
78
|
+
# @param progname [String, nil] The program name to match against the log entries.
|
|
79
|
+
# @param limit [Integer, nil] The maximum number of entries to return. If nil, all matching entries
|
|
80
|
+
# 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
|
+
# @return [Array<Lumberjack::LogEntry>] An array of log entries that match the specified filters.
|
|
84
|
+
def extract(message: nil, severity: nil, attributes: nil, progname: nil, limit: nil, level: nil, tags: nil)
|
|
85
|
+
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
|
+
|
|
95
|
+
matcher = LogEntryMatcher.new(message: message, severity: severity, attributes: attributes, progname: progname)
|
|
96
|
+
|
|
97
|
+
entries.each do |entry|
|
|
98
|
+
matched << entry if matcher.match?(entry)
|
|
99
|
+
break if limit && matched.size >= limit
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
matched
|
|
80
103
|
end
|
|
81
104
|
|
|
82
|
-
# Return true if the captured log entries match the specified level, message, and
|
|
105
|
+
# Return true if the captured log entries match the specified level, message, and attributes.
|
|
83
106
|
#
|
|
84
|
-
# For level, you can
|
|
107
|
+
# For level, you can specify either a numeric constant (i.e. `Logger::WARN`) or a symbol
|
|
85
108
|
# (i.e. `:warn`).
|
|
86
109
|
#
|
|
87
110
|
# For message you can specify a string to perform an exact match or a regular expression
|
|
88
111
|
# to perform a partial or pattern match. You can also supply any matcher value available
|
|
89
112
|
# in your test library (i.e. in rspec you could use `anything` or `instance_of(Error)`, etc.).
|
|
90
113
|
#
|
|
91
|
-
# For
|
|
92
|
-
# regular expression or matchers as the values here as well.
|
|
93
|
-
# nested
|
|
94
|
-
#
|
|
95
|
-
# Example:
|
|
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.
|
|
96
117
|
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
# ```
|
|
118
|
+
# @example
|
|
119
|
+
# logs.include?(level: :warn, message: /something happened/, attributes: {user: "john"})
|
|
100
120
|
#
|
|
101
|
-
# @param
|
|
102
|
-
# @option
|
|
103
|
-
# @option
|
|
104
|
-
# @option
|
|
105
|
-
# will match nested
|
|
106
|
-
#
|
|
107
|
-
# @option
|
|
121
|
+
# @param filters [Hash] The filters to apply to the captured entries.
|
|
122
|
+
# @option filters [String, Regexp] :message The message to match against the log entries.
|
|
123
|
+
# @option filters [String, Symbol, Integer] :severity The log level to match against the log entries.
|
|
124
|
+
# @option filters [Hash] :attributes A hash of attribute names to values to match against the log entries. The attributes
|
|
125
|
+
# will match nested attributes using dot notation (e.g. `foo.bar` will match an attribute with the structure
|
|
126
|
+
# +{foo: {bar: "value"}}+).
|
|
127
|
+
# @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.
|
|
108
130
|
# @return [Boolean] True if any entries match the specified filters, false otherwise.
|
|
109
|
-
def include?(
|
|
110
|
-
|
|
131
|
+
def include?(filters)
|
|
132
|
+
if filters.include?(:level)
|
|
133
|
+
Lumberjack::Utils.deprecated("Lumberjack::CaptureDevice#include?(level)", "Lumberjack::CaptureDevice#include? level option has been renamed to severity; it will be removed in version 2.1.")
|
|
134
|
+
end
|
|
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.")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
munged_filters = {
|
|
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)
|
|
111
148
|
end
|
|
112
149
|
|
|
113
|
-
# Return
|
|
114
|
-
# the same as described in the `include?` method.
|
|
150
|
+
# Return the first captured entry that matches the filters.
|
|
115
151
|
#
|
|
116
152
|
# @param message [String, Regexp, nil] The message to match against the log entries.
|
|
117
|
-
# @param
|
|
118
|
-
# @param
|
|
119
|
-
# will match nested
|
|
120
|
-
#
|
|
121
|
-
# @param
|
|
122
|
-
#
|
|
123
|
-
# @
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
# Normalize the level filter to numeric values.
|
|
129
|
-
level = (level.is_a?(Integer) ? level : Lumberjack::Severity.label_to_level(level))
|
|
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.")
|
|
130
164
|
end
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if matched?(entry, message, level, tags, progname)
|
|
134
|
-
matches << entry
|
|
135
|
-
break if limit && matches.size >= limit
|
|
136
|
-
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.")
|
|
137
167
|
end
|
|
138
168
|
|
|
139
|
-
|
|
169
|
+
super(message: message, severity: severity || level, attributes: attributes || tags, progname: progname)
|
|
140
170
|
end
|
|
141
171
|
|
|
142
172
|
# Return the log entry that most closely matches the specified filters. This method
|
|
@@ -145,118 +175,75 @@ module Lumberjack
|
|
|
145
175
|
# they match. Returns nil if no entry meets the minimum matching criteria.
|
|
146
176
|
#
|
|
147
177
|
# @param message [String, Regexp, nil] The message to match against the log entries.
|
|
148
|
-
# @param
|
|
149
|
-
# @param
|
|
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.
|
|
150
180
|
# @param progname [String, nil] The program name to match against the log entries.
|
|
151
|
-
# @
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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.")
|
|
158
189
|
end
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
best_score = 0
|
|
162
|
-
|
|
163
|
-
@buffer.each do |entry|
|
|
164
|
-
score = Lumberjack::CaptureDevice::EntryScore.calculate_match_score(entry, message, level, tags, progname)
|
|
165
|
-
if score > best_score && score >= Lumberjack::CaptureDevice::EntryScore::MIN_SCORE_THRESHOLD
|
|
166
|
-
best_score = score
|
|
167
|
-
best_entry = entry
|
|
168
|
-
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.")
|
|
169
192
|
end
|
|
170
193
|
|
|
171
|
-
|
|
194
|
+
super(
|
|
195
|
+
message: message,
|
|
196
|
+
severity: severity || level,
|
|
197
|
+
attributes: attributes || tags,
|
|
198
|
+
progname: progname
|
|
199
|
+
)
|
|
172
200
|
end
|
|
173
201
|
|
|
202
|
+
# Write the captured log entries to the underlying device.
|
|
203
|
+
#
|
|
204
|
+
# @return [void]
|
|
205
|
+
def write_to_underlying_device
|
|
206
|
+
write_to(@underlying_device) if @underlying_device
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Provide a detailed string representation showing all captured entries.
|
|
210
|
+
#
|
|
211
|
+
# @return [String] A formatted string showing all captured log entries.
|
|
174
212
|
def inspect
|
|
175
|
-
message = +"<##{self.class.name} #{
|
|
176
|
-
|
|
177
|
-
|
|
213
|
+
message = +"<##{self.class.name} #{length} #{(length == 1) ? "entry" : "entries"} captured:\n"
|
|
214
|
+
template = Lumberjack::LocalLogTemplate.new
|
|
215
|
+
entries.each do |entry|
|
|
216
|
+
formatted = template.call(entry).split("\n").collect { |line| " #{line}" }.join("\n")
|
|
217
|
+
message << formatted
|
|
218
|
+
message << "\n"
|
|
178
219
|
end
|
|
179
|
-
message << "
|
|
220
|
+
message << ">"
|
|
180
221
|
message
|
|
181
222
|
end
|
|
182
223
|
|
|
224
|
+
# Provide a simple string representation showing the count of captured entries.
|
|
225
|
+
#
|
|
226
|
+
# @return [String] A brief description of the captured entries count.
|
|
183
227
|
def to_s
|
|
184
|
-
"<##{self.class.name} #{
|
|
228
|
+
"<##{self.class.name} #{length} #{(length == 1) ? "entry" : "entries"} captured>"
|
|
185
229
|
end
|
|
186
230
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return false unless match?(entry.progname, progname_filter)
|
|
193
|
-
|
|
194
|
-
if tags_filter.is_a?(Hash)
|
|
195
|
-
tags_filter = deep_stringify_keys(Lumberjack::Utils.expand_tags(tags_filter))
|
|
196
|
-
end
|
|
197
|
-
tags = deep_stringify_keys(Lumberjack::Utils.expand_tags(entry.tags))
|
|
198
|
-
|
|
199
|
-
return false unless match_tags?(tags, tags_filter)
|
|
200
|
-
|
|
201
|
-
true
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def match?(value, filter)
|
|
205
|
-
return true unless filter
|
|
206
|
-
|
|
207
|
-
filter === value
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def match_tags?(tags, filter)
|
|
211
|
-
return true unless filter
|
|
212
|
-
return false unless tags
|
|
213
|
-
|
|
214
|
-
filter.all? do |name, value_filter|
|
|
215
|
-
name = name.to_s
|
|
216
|
-
tag_values = tags[name]
|
|
217
|
-
if tag_values.is_a?(Hash)
|
|
218
|
-
if value_filter.is_a?(Hash)
|
|
219
|
-
match_tags?(tag_values, value_filter)
|
|
220
|
-
else
|
|
221
|
-
false
|
|
222
|
-
end
|
|
223
|
-
elsif value_filter.nil? || (value_filter.is_a?(Enumerable) && value_filter.empty?)
|
|
224
|
-
tag_values.nil? || (tag_values.is_a?(Array) && tag_values.empty?)
|
|
225
|
-
elsif tags.include?(name)
|
|
226
|
-
match?(tag_values, value_filter)
|
|
227
|
-
else
|
|
228
|
-
false
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
+
# Return the number of captured log entries.
|
|
232
|
+
#
|
|
233
|
+
# @return [Integer] The number of captured entries.
|
|
234
|
+
def length
|
|
235
|
+
@buffer.length
|
|
231
236
|
end
|
|
232
237
|
|
|
233
|
-
|
|
234
|
-
if hash.is_a?(Hash)
|
|
235
|
-
hash.each_with_object({}) do |(key, value), result|
|
|
236
|
-
new_key = key.to_s
|
|
237
|
-
new_value = deep_stringify_keys(value)
|
|
238
|
-
result[new_key] = new_value
|
|
239
|
-
end
|
|
240
|
-
elsif hash.is_a?(Enumerable)
|
|
241
|
-
hash.collect { |item| deep_stringify_keys(item) }
|
|
242
|
-
else
|
|
243
|
-
hash
|
|
244
|
-
end
|
|
245
|
-
end
|
|
238
|
+
alias_method :size, :length
|
|
246
239
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
formatted
|
|
240
|
+
# Iterate over each captured log entry.
|
|
241
|
+
#
|
|
242
|
+
# @yield [entry] Block to execute for each captured entry.
|
|
243
|
+
# @yieldparam entry [Lumberjack::LogEntry] A captured log entry.
|
|
244
|
+
# @return [Array<Lumberjack::LogEntry>] The captured entries (when no block given).
|
|
245
|
+
def each(&block)
|
|
246
|
+
@buffer.each(&block)
|
|
257
247
|
end
|
|
258
248
|
end
|
|
259
249
|
end
|
|
260
|
-
|
|
261
|
-
require_relative "capture_device/entry_score"
|
|
262
|
-
require_relative "capture_device/include_log_entry_matcher"
|
|
@@ -31,8 +31,8 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
|
|
32
32
|
spec.require_paths = ["lib"]
|
|
33
33
|
|
|
34
|
-
spec.required_ruby_version = ">= 2.
|
|
34
|
+
spec.required_ruby_version = ">= 2.7"
|
|
35
35
|
|
|
36
|
-
spec.add_dependency "lumberjack", ">=
|
|
36
|
+
spec.add_dependency "lumberjack", ">=2.0"
|
|
37
37
|
spec.add_development_dependency "bundler"
|
|
38
38
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lumberjack_capture_device
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: lumberjack
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
18
|
+
version: '2.0'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
25
|
+
version: '2.0'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: bundler
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -38,7 +37,6 @@ dependencies:
|
|
|
38
37
|
- - ">="
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: '0'
|
|
41
|
-
description:
|
|
42
40
|
email:
|
|
43
41
|
- bbdurand@gmail.com
|
|
44
42
|
executables: []
|
|
@@ -62,7 +60,6 @@ metadata:
|
|
|
62
60
|
homepage_uri: https://github.com/bdurand/lumberjack_capture_device
|
|
63
61
|
source_code_uri: https://github.com/bdurand/lumberjack_capture_device
|
|
64
62
|
changelog_uri: https://github.com/bdurand/lumberjack_capture_device/blob/main/CHANGELOG.md
|
|
65
|
-
post_install_message:
|
|
66
63
|
rdoc_options: []
|
|
67
64
|
require_paths:
|
|
68
65
|
- lib
|
|
@@ -70,15 +67,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
70
67
|
requirements:
|
|
71
68
|
- - ">="
|
|
72
69
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: '2.
|
|
70
|
+
version: '2.7'
|
|
74
71
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
72
|
requirements:
|
|
76
73
|
- - ">="
|
|
77
74
|
- !ruby/object:Gem::Version
|
|
78
75
|
version: '0'
|
|
79
76
|
requirements: []
|
|
80
|
-
rubygems_version: 3.
|
|
81
|
-
signing_key:
|
|
77
|
+
rubygems_version: 3.6.9
|
|
82
78
|
specification_version: 4
|
|
83
79
|
summary: Testing device for the lumberjack gem that can be used for asserting messages
|
|
84
80
|
have been logged in a test suite.
|