lumberjack_capture_device 1.2.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9147e66264c0eeebb01f7c04df1b6e18ab64bc4db3337e968d6cd1d2201fef7f
4
- data.tar.gz: f01bf2e332024e9ef26ef703b374956e5f17c6bb4f351cb4518b1591b62dd992
3
+ metadata.gz: be90765c9b26b0ca9c501504e83c2a58cf1f2ce74edffe0b5e3d8e2515194277
4
+ data.tar.gz: '0825d9fa6621df4c5cdea31de794e1c8f263249d3f42f0e17945806ccb7f5f69'
5
5
  SHA512:
6
- metadata.gz: 5e5ae9f94313a18f793b299998c9760aaa8d7bf5d8f462f48b4519f83c8844893ff894e5b570e31b15d55dc1ad9d25ee723529dc72e51db83d5060a4bcf1597c
7
- data.tar.gz: 06a11ff5e4c5f3afed7cda12fa2f5f9bf94369f6c5b382d7118622136b7f1507d67c1e68ca472c6c38d1bf38cd3aa6db66f4859a145f78b9b69a8f9f4024a634
6
+ metadata.gz: 5423feff017c2b44974d7e75268c39388353165128632d0b7533619b5271ff17aeb1e9250d7ceda017de56f272baf0bda9b6e2a180a98397b13520b128f5449b
7
+ data.tar.gz: 3bbdb6b6ca43683a3ec20ead94fa93e557b33194c4ee58d5f36c478e50b01e7ff5abed29a3001fce885f47f304f62d3bcbeec5cddd00708401f377db7d775bf1
data/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ 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
+
7
22
  ## 1.2.2
8
23
 
9
24
  ### 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 tags.
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 tags argument can match tags with a Hash mapping tag names to the matcher values. If tags are nested, you can use dot notation on tag names to reference nested tags.
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, tags: {foo: "bar"})
56
- expect(logs).to include(tags: {foo: anything, count: {one: 1}})
57
- expect(logs).to include(tags: {foo: anything, "count.one" => 1})
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.2
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
- def calculate_match_score(entry, message_filter, level_filter, tags_filter, progname_filter)
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 level match
23
- if level_filter
24
- level_score = if entry.severity == level_filter
25
- 1.0 # Exact level match
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
- level_proximity_score(entry.severity, level_filter) # Partial level match
36
+ severity_proximity_score(entry.severity, severity_filter) # Partial severity match
28
37
  end
29
- scores << level_score
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 tags match
41
- if tags_filter.is_a?(Hash) && !tags_filter.empty?
42
- tags_score = calculate_tags_score(entry.tags, tags_filter)
43
- scores << tags_score
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 level distance
96
- def level_proximity_score(entry_level, filter_level)
97
- level_diff = (entry_level - filter_level).abs
98
- case level_diff
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 tag matching
107
- def calculate_tags_score(entry_tags, tags_filter)
108
- return 0.0 unless entry_tags && tags_filter.is_a?(Hash)
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
- tags_filter = deep_stringify_keys(Lumberjack::Utils.expand_tags(tags_filter))
111
- tags = deep_stringify_keys(Lumberjack::Utils.expand_tags(entry_tags))
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
- total_tag_filters = count_tag_filters(tags_filter)
114
- return 0.0 if total_tag_filters == 0
137
+ total_attribute_filters = count_attribute_filters(attributes_filter)
138
+ return 0.0 if total_attribute_filters == 0
115
139
 
116
- matched_tags = count_matched_tags(tags, tags_filter)
117
- matched_tags.to_f / total_tag_filters
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
- def count_tag_filters(tags_filter, count = 0)
175
- tags_filter.each do |_name, value_filter|
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 = count_tag_filters(value_filter, 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
- def count_matched_tags(tags, tags_filter, count = 0)
186
- return count unless tags && tags_filter
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
- tags_filter.each do |name, value_filter|
234
+ attributes_filter.each do |name, value_filter|
189
235
  name = name.to_s
190
- tag_values = tags[name]
236
+ attribute_values = attributes[name]
191
237
 
192
- if value_filter.is_a?(Hash) && tag_values.is_a?(Hash)
193
- count = count_matched_tags(tag_values, value_filter, count)
194
- elsif tags.include?(name) && exact_match?(tag_values, value_filter)
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,91 +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
- @captured_logger = nil
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
- @captured_logger = actual
12
- return false unless valid_captured_logger?
19
+ @logger = actual
20
+ return false unless valid_logger?
13
21
 
14
- @captured_logger.include?(@expected_hash)
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 valid_captured_logger?
19
- formatted_failure_message(@captured_logger, @expected_hash)
30
+ if valid_logger?
31
+ formatted_failure_message(@logger, @expected_hash)
20
32
  else
21
- wrong_object_type_message(@captured_logger)
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 valid_captured_logger?
27
- formatted_negated_failure_message(@captured_logger, @expected_hash)
41
+ if valid_logger?
42
+ formatted_negated_failure_message(@logger, @expected_hash)
28
43
  else
29
- wrong_object_type_message(@captured_logger)
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
- def valid_captured_logger?
40
- @captured_logger.is_a?(Lumberjack::CaptureDevice)
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
- def wrong_object_type_message(captured_logger)
44
- "Expected a Lumberjack::CaptureDevice object, but received a #{captured_logger.class}."
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
- def formatted_failure_message(captured_logger, expected_hash)
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
95
+
48
96
  message = +"expected logs to include entry:\n" \
49
- "#{Lumberjack::CaptureDevice.formatted_expectation(expected_hash, indent: 2)}\n\n" \
50
- "Captured #{captured_logger.length} log #{(captured_logger.length == 1) ? "entry" : "entries"}"
97
+ "#{Lumberjack::Device::Test.formatted_expectation(expected_hash, indent: 2)}"
51
98
 
52
- if captured_logger.length > 0
53
- message << "\n----------------------\n"
54
- captured_logger.each do |entry|
55
- message << "#{Lumberjack::CaptureDevice.formatted_entry(entry)}\n"
56
- end
99
+ closest_match = device.closest_match(**expected_hash)
100
+ if closest_match
101
+ message << "\n\nClosest match found:" \
102
+ "#{Lumberjack::Device::Test.formatted_expectation(closest_match, indent: 2)}"
57
103
  end
58
104
 
59
- closest_match = captured_logger.closest_match(**expected_hash)
60
- if closest_match
61
- message = "#{message}\n\nClosest match found:" \
62
- "#{Lumberjack::CaptureDevice.formatted_expectation(closest_match, indent: 2)}"
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
63
113
  end
64
114
 
65
115
  message
66
116
  end
67
117
 
68
- def formatted_negated_failure_message(captured_logger, expected_hash)
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
69
125
  message = "expected logs not to include entry:\n" \
70
- "#{Lumberjack::CaptureDevice.formatted_expectation(expected_hash, indent: 2)}"
126
+ "#{Lumberjack::Device::Test.formatted_expectation(expected_hash, indent: 2)}"
71
127
 
72
- match = captured_logger.match(**expected_hash)
128
+ match = device.match(**expected_hash)
73
129
  if match
74
130
  message = "#{message}\n\nFound entry:\n" \
75
- "#{Lumberjack::CaptureDevice.formatted_expectation(match, indent: 2)}"
131
+ "#{Lumberjack::Device::Test.formatted_expectation(match, indent: 2)}"
76
132
  end
77
133
 
78
134
  message
79
135
  end
80
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.
81
141
  def expectation_description(expected_hash)
82
142
  info = []
83
- info << "level: #{expected_hash[:level].inspect}" unless expected_hash[:level].nil?
143
+ info << "severity: #{expected_hash[:severity].inspect}" unless expected_hash[:severity].nil?
84
144
  info << "message: #{expected_hash[:message].inspect}" unless expected_hash[:message].nil?
85
145
  info << "progname: #{expected_hash[:progname].inspect}" unless expected_hash[:progname].nil?
86
- if expected_hash[:tags].is_a?(Hash) && !expected_hash[:tags].empty?
87
- tags = Lumberjack::Utils.flatten_tags(expected_hash[:tags])
88
- tags_info = tags.collect { |name, value| "#{name}=#{value.inspect}" }.join(", ")
89
- info << "tags: #{tags_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}"
90
150
  end
91
151
  info.join(", ")
92
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
- def capture_logger(logger, &block)
12
- Lumberjack::CaptureDevice.capture(logger, &block)
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,12 +5,13 @@ 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
+
11
+ require_relative "capture_device/include_log_entry_matcher"
10
12
 
11
13
  include Enumerable
12
14
 
13
- attr_reader :buffer
14
15
  class << self
15
16
  # Capture the entries written by the logger within a block. Within the block all log
16
17
  # entries will be written to a CaptureDevice rather than to the normal output for
@@ -19,169 +20,153 @@ module Lumberjack
19
20
  # by the method call.
20
21
  #
21
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.
22
26
  # @yield [device] The block to execute while capturing log entries.
23
27
  # @return [Lumberjack::CaptureDevice] The device that captured the log entries.
24
28
  # @yieldparam device [Lumberjack::CaptureDevice] The device that will capture the log entries.
25
29
  # @example
26
30
  # Lumberjack::CaptureDevice.capture(logger) do |logs|
27
31
  # logger.info("This will be captured")
28
- # expect(logs).to include(level: :info, message: "This will be captured")
32
+ # expect(logs).to include(severity: :info, message: "This will be captured")
29
33
  # end
30
34
  #
31
35
  # @example
32
36
  # logs = Lumberjack::CaptureDevice.capture(logger) { logger.info("This will be captured") }
33
- # expect(logs).to include(level: :info, message: "This will be captured")
34
- def capture(logger)
35
- device = new
37
+ # expect(logs).to include(severity: :info, message: "This will be captured")
38
+ def capture(logger, write_to_original: true)
36
39
  save_device = logger.device
37
40
  save_level = logger.level
38
- save_formatter = logger.formatter
41
+ device = new(underlying_device: save_device)
42
+
39
43
  begin
40
44
  logger.device = device
41
45
  logger.level = :debug
42
- logger.formatter = Lumberjack::Formatter.empty
43
46
  yield device
44
47
  ensure
45
48
  logger.device = save_device
46
49
  logger.level = save_level
47
- logger.formatter = save_formatter
50
+ device.write_to_underlying_device if write_to_original
48
51
  end
49
- device
50
- end
51
52
 
52
- # Helper method to format a log entry for display.
53
- #
54
- # @param entry [Lumberjack::LogEntry] The log entry to format.
55
- # @param indent [Integer] The indentation to prefix on every line.
56
- # @return [String] The formatted log entry.
57
- def formatted_entry(entry, indent: 0)
58
- indent_str = " " * indent
59
- timestamp = entry.time.strftime("%Y-%m-%d %H:%M:%S")
60
- formatted = +"#{indent_str}#{timestamp} #{entry.severity_label}: #{entry.message}"
61
- formatted << "\n#{indent_str} progname: #{entry.progname}" if entry.progname.to_s != ""
62
- if entry.tags && !entry.tags.empty?
63
- Lumberjack::Utils.flatten_tags(entry.tags).to_a.sort_by(&:first).each do |name, value|
64
- formatted << "\n#{indent_str} #{name}: #{value}"
65
- end
66
- end
67
- formatted
53
+ device
68
54
  end
55
+ end
69
56
 
70
- # Format a log entry or expectation hash into a more human readable format.
71
- #
72
- # @param expectation [Hash, Lumberjack::LogEntry] The expectation or log entry to format.
73
- # @return [String] A formatted string representation of the expectation or log entry.
74
- def formatted_expectation(expectation, indent: 0)
75
- if expectation.is_a?(Lumberjack::LogEntry)
76
- expectation = {
77
- "level" => expectation.severity_label,
78
- "message" => expectation.message,
79
- "progname" => expectation.progname,
80
- "tags" => expectation.tags
81
- }
82
- end
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
83
61
 
84
- expectation = expectation.transform_keys(&:to_s).compact
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))
68
+ end
85
69
 
86
- message = []
87
- indent_str = " " * indent
88
- message << "#{indent_str}level: #{expectation["level"].inspect}" if expectation.include?("level")
89
- message << "#{indent_str}message: #{expectation["message"].inspect}" if expectation.include?("message")
90
- message << "#{indent_str}progname: #{expectation["progname"].inspect}" if expectation.include?("progname")
91
- if expectation["tags"].is_a?(Hash) && !expectation["tags"].empty?
92
- tags = Lumberjack::Utils.flatten_tags(expectation["tags"])
93
- prefix = "tags: "
94
- tags.sort_by(&:first).each do |name, value|
95
- message << "#{prefix} #{name}: #{value.inspect}"
96
- prefix = "#{indent_str} "
97
- end
98
- end
99
- message.join("\n")
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
100
93
  end
101
- end
102
94
 
103
- def initialize
104
- @buffer = []
105
- end
95
+ matcher = LogEntryMatcher.new(message: message, severity: severity, attributes: attributes, progname: progname)
106
96
 
107
- def write(entry)
108
- @buffer << entry
109
- end
97
+ entries.each do |entry|
98
+ matched << entry if matcher.match?(entry)
99
+ break if limit && matched.size >= limit
100
+ end
110
101
 
111
- # Clear all entries that have been written to the buffer.
112
- def clear
113
- @buffer.clear
102
+ matched
114
103
  end
115
104
 
116
- # Return true if the captured log entries match the specified level, message, and tags.
105
+ # Return true if the captured log entries match the specified level, message, and attributes.
117
106
  #
118
- # For level, you can specified either a numeric constant (i.e. `Logger::WARN`) or a symbol
107
+ # For level, you can specify either a numeric constant (i.e. `Logger::WARN`) or a symbol
119
108
  # (i.e. `:warn`).
120
109
  #
121
110
  # For message you can specify a string to perform an exact match or a regular expression
122
111
  # to perform a partial or pattern match. You can also supply any matcher value available
123
112
  # in your test library (i.e. in rspec you could use `anything` or `instance_of(Error)`, etc.).
124
113
  #
125
- # For tags, you can specify a hash of tag names to values to match. You can use
126
- # regular expression or matchers as the values here as well. Tags can also be nested to match
127
- # nested tags.
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.
128
117
  #
129
- # Example:
118
+ # @example
119
+ # logs.include?(level: :warn, message: /something happened/, attributes: {user: "john"})
130
120
  #
131
- # ```
132
- # logs.include(level: :warn, message: /something happened/, tags: {duration: instance_of(Float)})
133
- # ```
134
- #
135
- # @param args [Hash] The filters to apply to the captured entries.
136
- # @option args [String, Regexp] :message The message to match against the log entries.
137
- # @option args [String, Symbol, Integer] :level The log level to match against the log entries.
138
- # @option args [Hash] :tags A hash of tag names to values to match against the log entries. The tags
139
- # will match nested tags using dot notation (e.g. `foo.bar` will match a tag with the structure
140
- # `{foo: {bar: "value"}}`).
141
- # @option args [String] :progname The program name to match against the log entries.
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.
142
130
  # @return [Boolean] True if any entries match the specified filters, false otherwise.
143
- def include?(args)
144
- !!match(**args)
145
- end
146
-
147
- # Return all the captured entries that match the specified filters. These filters are
148
- # the same as described in the `include?` method.
149
- #
150
- # @param message [String, Regexp, nil] The message to match against the log entries.
151
- # @param level [String, Symbol, Integer, nil] The log level to match against the log entries.
152
- # @param tags [Hash, nil] A hash of tag names to values to match against the log entries. The tags
153
- # will match nested tags using dot notation (e.g. `foo.bar` will match a tag with the structure
154
- # `{foo: {bar: "value"}}`).
155
- # @param limit [Integer, nil] The maximum number of entries to return. If nil, all matching entries
156
- # will be returned.
157
- # @return [Array<Lumberjack::LogEntry>] An array of log entries that match the specified filters.
158
- def extract(message: nil, level: nil, tags: nil, progname: nil, limit: nil)
159
- matches = []
160
-
161
- if level
162
- # Normalize the level filter to numeric values.
163
- level = (level.is_a?(Integer) ? level : Lumberjack::Severity.label_to_level(level))
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.")
164
134
  end
165
135
 
166
- @buffer.each do |entry|
167
- if matched?(entry, message, level, tags, progname)
168
- matches << entry
169
- break if limit && matches.size >= limit
170
- end
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.")
171
138
  end
172
139
 
173
- matches
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)
174
148
  end
175
149
 
176
- # Return the first entry that matches the specified filters.
150
+ # Return the first captured entry that matches the filters.
177
151
  #
178
152
  # @param message [String, Regexp, nil] The message to match against the log entries.
179
- # @param level [String, Symbol, Integer, nil] The log level to match against the log entries.
180
- # @param tags [Hash, nil] A hash of tag names to values 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"}}+).
181
157
  # @param progname [String, nil] The program name to match against the log entries.
182
- # @return [Lumberjack::LogEntry, nil] The log entry that most closely matches the filters, or nil if no entry meets minimum criteria.
183
- def match(message: nil, level: nil, tags: nil, progname: nil)
184
- extract(message: message, level: level, tags: tags, progname: progname, limit: 1).first
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)
185
170
  end
186
171
 
187
172
  # Return the log entry that most closely matches the specified filters. This method
@@ -190,119 +175,75 @@ module Lumberjack
190
175
  # they match. Returns nil if no entry meets the minimum matching criteria.
191
176
  #
192
177
  # @param message [String, Regexp, nil] The message to match against the log entries.
193
- # @param level [String, Symbol, Integer, nil] The log level to match against the log entries.
194
- # @param tags [Hash, nil] A hash of tag names to values 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.
195
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.
196
183
  # @return [Lumberjack::LogEntry, nil] The log entry that most closely matches the filters, or nil if no entry meets minimum criteria.
197
- def closest_match(message: nil, level: nil, tags: nil, progname: nil)
198
- return nil if @buffer.empty?
199
-
200
- exact_match = match(message: message, level: level, tags: tags, progname: progname)
201
- return exact_match if exact_match
184
+ def closest_match(message: nil, severity: nil, attributes: nil, progname: nil, level: nil, tags: nil)
185
+ return nil if length == 0
202
186
 
203
- # Normalize level filter
204
- if level
205
- level = (level.is_a?(Integer) ? level : Lumberjack::Severity.label_to_level(level))
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.")
206
189
  end
207
-
208
- best_entry = nil
209
- best_score = 0
210
-
211
- @buffer.each do |entry|
212
- score = Lumberjack::CaptureDevice::EntryScore.calculate_match_score(entry, message, level, tags, progname)
213
- if score > best_score && score >= Lumberjack::CaptureDevice::EntryScore::MIN_SCORE_THRESHOLD
214
- best_score = score
215
- best_entry = entry
216
- 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.")
217
192
  end
218
193
 
219
- best_entry
194
+ super(
195
+ message: message,
196
+ severity: severity || level,
197
+ attributes: attributes || tags,
198
+ progname: progname
199
+ )
220
200
  end
221
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.
222
212
  def inspect
223
- message = +"<##{self.class.name} #{@buffer.size} #{(@buffer.size == 1) ? "entry" : "entries"} captured:"
224
- @buffer.each do |entry|
225
- message << "\n #{Lumberjack::CaptureDevice.formatted_entry(entry)}"
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"
226
219
  end
227
- message << "\n>"
220
+ message << ">"
228
221
  message
229
222
  end
230
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.
231
227
  def to_s
232
- "<##{self.class.name} #{@buffer.size} #{(@buffer.size == 1) ? "entry" : "entries"} captured>"
228
+ "<##{self.class.name} #{length} #{(length == 1) ? "entry" : "entries"} captured>"
233
229
  end
234
230
 
231
+ # Return the number of captured log entries.
232
+ #
233
+ # @return [Integer] The number of captured entries.
235
234
  def length
236
235
  @buffer.length
237
236
  end
238
237
 
239
238
  alias_method :size, :length
240
239
 
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).
241
245
  def each(&block)
242
246
  @buffer.each(&block)
243
247
  end
244
-
245
- private
246
-
247
- def matched?(entry, message_filter, level_filter, tags_filter, progname_filter)
248
- return false unless match?(entry.message, message_filter)
249
- return false unless match?(entry.severity, level_filter)
250
- return false unless match?(entry.progname, progname_filter)
251
-
252
- if tags_filter.is_a?(Hash)
253
- tags_filter = deep_stringify_keys(Lumberjack::Utils.expand_tags(tags_filter))
254
- end
255
- tags = deep_stringify_keys(Lumberjack::Utils.expand_tags(entry.tags))
256
-
257
- return false unless match_tags?(tags, tags_filter)
258
-
259
- true
260
- end
261
-
262
- def match?(value, filter)
263
- return true unless filter
264
-
265
- filter === value
266
- end
267
-
268
- def match_tags?(tags, filter)
269
- return true unless filter
270
- return false unless tags
271
-
272
- filter.all? do |name, value_filter|
273
- name = name.to_s
274
- tag_values = tags[name]
275
- if tag_values.is_a?(Hash)
276
- if value_filter.is_a?(Hash)
277
- match_tags?(tag_values, value_filter)
278
- else
279
- false
280
- end
281
- elsif value_filter.nil? || (value_filter.is_a?(Enumerable) && value_filter.empty?)
282
- tag_values.nil? || (tag_values.is_a?(Array) && tag_values.empty?)
283
- elsif tags.include?(name)
284
- match?(tag_values, value_filter)
285
- else
286
- false
287
- end
288
- end
289
- end
290
-
291
- def deep_stringify_keys(hash)
292
- if hash.is_a?(Hash)
293
- hash.each_with_object({}) do |(key, value), result|
294
- new_key = key.to_s
295
- new_value = deep_stringify_keys(value)
296
- result[new_key] = new_value
297
- end
298
- elsif hash.is_a?(Enumerable)
299
- hash.collect { |item| deep_stringify_keys(item) }
300
- else
301
- hash
302
- end
303
- end
304
248
  end
305
249
  end
306
-
307
- require_relative "capture_device/entry_score"
308
- 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.5"
34
+ spec.required_ruby_version = ">= 2.7"
35
35
 
36
- spec.add_dependency "lumberjack", ">=1.3.3"
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: 1.2.2
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: 2025-08-09 00:00:00.000000000 Z
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: 1.3.3
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: 1.3.3
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.5'
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.4.10
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.