lumberjack 1.4.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +524 -176
  3. data/CHANGELOG.md +89 -0
  4. data/README.md +604 -211
  5. data/UPGRADE_GUIDE.md +80 -0
  6. data/VERSION +1 -1
  7. data/lib/lumberjack/attribute_formatter.rb +451 -0
  8. data/lib/lumberjack/attributes_helper.rb +100 -0
  9. data/lib/lumberjack/context.rb +120 -23
  10. data/lib/lumberjack/context_logger.rb +620 -0
  11. data/lib/lumberjack/device/buffer.rb +209 -0
  12. data/lib/lumberjack/device/date_rolling_log_file.rb +10 -62
  13. data/lib/lumberjack/device/log_file.rb +76 -29
  14. data/lib/lumberjack/device/logger_wrapper.rb +137 -0
  15. data/lib/lumberjack/device/multi.rb +92 -30
  16. data/lib/lumberjack/device/null.rb +26 -8
  17. data/lib/lumberjack/device/size_rolling_log_file.rb +13 -54
  18. data/lib/lumberjack/device/test.rb +337 -0
  19. data/lib/lumberjack/device/writer.rb +184 -176
  20. data/lib/lumberjack/device.rb +134 -15
  21. data/lib/lumberjack/device_registry.rb +90 -0
  22. data/lib/lumberjack/entry_formatter.rb +357 -0
  23. data/lib/lumberjack/fiber_locals.rb +55 -0
  24. data/lib/lumberjack/forked_logger.rb +143 -0
  25. data/lib/lumberjack/formatter/date_time_formatter.rb +14 -3
  26. data/lib/lumberjack/formatter/exception_formatter.rb +12 -2
  27. data/lib/lumberjack/formatter/id_formatter.rb +13 -1
  28. data/lib/lumberjack/formatter/inspect_formatter.rb +14 -1
  29. data/lib/lumberjack/formatter/multiply_formatter.rb +10 -0
  30. data/lib/lumberjack/formatter/object_formatter.rb +13 -1
  31. data/lib/lumberjack/formatter/pretty_print_formatter.rb +15 -2
  32. data/lib/lumberjack/formatter/redact_formatter.rb +18 -3
  33. data/lib/lumberjack/formatter/round_formatter.rb +12 -0
  34. data/lib/lumberjack/formatter/string_formatter.rb +9 -1
  35. data/lib/lumberjack/formatter/strip_formatter.rb +13 -1
  36. data/lib/lumberjack/formatter/structured_formatter.rb +18 -2
  37. data/lib/lumberjack/formatter/tagged_message.rb +10 -32
  38. data/lib/lumberjack/formatter/tags_formatter.rb +32 -0
  39. data/lib/lumberjack/formatter/truncate_formatter.rb +8 -1
  40. data/lib/lumberjack/formatter.rb +271 -141
  41. data/lib/lumberjack/formatter_registry.rb +84 -0
  42. data/lib/lumberjack/io_compatibility.rb +133 -0
  43. data/lib/lumberjack/local_log_template.rb +209 -0
  44. data/lib/lumberjack/log_entry.rb +154 -79
  45. data/lib/lumberjack/log_entry_matcher/score.rb +276 -0
  46. data/lib/lumberjack/log_entry_matcher.rb +126 -0
  47. data/lib/lumberjack/logger.rb +328 -556
  48. data/lib/lumberjack/message_attributes.rb +38 -0
  49. data/lib/lumberjack/rack/context.rb +66 -15
  50. data/lib/lumberjack/rack.rb +0 -2
  51. data/lib/lumberjack/remap_attribute.rb +24 -0
  52. data/lib/lumberjack/severity.rb +52 -15
  53. data/lib/lumberjack/tag_context.rb +8 -71
  54. data/lib/lumberjack/tag_formatter.rb +22 -188
  55. data/lib/lumberjack/tags.rb +15 -21
  56. data/lib/lumberjack/template.rb +252 -62
  57. data/lib/lumberjack/template_registry.rb +60 -0
  58. data/lib/lumberjack/utils.rb +198 -48
  59. data/lib/lumberjack.rb +167 -59
  60. data/lumberjack.gemspec +4 -2
  61. metadata +41 -15
  62. data/lib/lumberjack/device/rolling_log_file.rb +0 -145
  63. data/lib/lumberjack/rack/request_id.rb +0 -31
  64. data/lib/lumberjack/rack/unit_of_work.rb +0 -21
  65. data/lib/lumberjack/tagged_logger_support.rb +0 -81
  66. data/lib/lumberjack/tagged_logging.rb +0 -29
@@ -1,15 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lumberjack
4
- class Device
5
- # This is a logging device that produces no output. It can be useful in
6
- # testing environments when log file output is not useful.
7
- class Null < Device
8
- def initialize(*args)
9
- end
4
+ # A logging device that discards all output. This device provides a silent
5
+ # logging implementation useful for testing environments, performance benchmarks,
6
+ # or production scenarios where logging needs to be temporarily disabled without
7
+ # changing logger configuration.
8
+ #
9
+ # The Null device implements the complete Device interface but performs no
10
+ # actual operations, making it both efficient and transparent. It accepts
11
+ # any constructor arguments for compatibility but ignores them all.
12
+ #
13
+ # @example Creating a silent logger
14
+ # logger = Lumberjack::Logger.new(Lumberjack::Device::Null.new)
15
+ # logger.info("This message is discarded")
16
+ #
17
+ # @example Using the convenience constructor
18
+ # logger = Lumberjack::Logger.new(:null)
19
+ # logger.error("This error is also discarded")
20
+ class Device::Null < Device
21
+ DeviceRegistry.add(:null, self)
10
22
 
11
- def write(entry)
12
- end
23
+ def initialize(*args)
24
+ end
25
+
26
+ # Discard the log entry without performing any operation.
27
+ #
28
+ # @param entry [Lumberjack::LogEntry] The log entry to discard.
29
+ # @return [void]
30
+ def write(entry)
13
31
  end
14
32
  end
15
33
  end
@@ -1,63 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lumberjack
4
- class Device
5
- # This is a log device that appends entries to a file and rolls the file when it reaches a specified
6
- # size threshold. When a file is rolled, it will have an number extension appended to the file name.
7
- # For example, if the log file is named production.log, the first time it is rolled it will be renamed
8
- # production.log.1, then production.log.2, etc.
9
- class SizeRollingLogFile < RollingLogFile
10
- attr_reader :max_size
4
+ # Deprecated device. Use LogFile instead.
5
+ #
6
+ # @deprecated Use Lumberjack::Device::LogFile
7
+ class Device::SizeRollingLogFile < Device::LogFile
8
+ attr_reader :max_size
11
9
 
12
- # Create an new log device to the specified file. The maximum size of the log file is specified with
13
- # the :max_size option. The unit can also be specified: "32K", "100M", "2G" are all valid.
14
- def initialize(path, options = {})
15
- @manual = options[:manual]
16
- @max_size = options[:max_size]
17
- if @max_size.is_a?(String)
18
- if @max_size =~ /^(\d+(\.\d+)?)([KMG])?$/i
19
- @max_size = $~[1].to_f
20
- units = $~[3].to_s.upcase
21
- case units
22
- when "K"
23
- @max_size *= 1024
24
- when "M"
25
- @max_size *= 1024**2
26
- when "G"
27
- @max_size *= 1024**3
28
- end
29
- @max_size = @max_size.round
30
- else
31
- raise ArgumentError.new("illegal value for :max_size (#{@max_size})")
32
- end
33
- end
10
+ # Create an new log device to the specified file. The maximum size of the log file is specified with
11
+ # the :max_size option. The unit can also be specified: "32K", "100M", "2G" are all valid.
12
+ def initialize(path, options = {})
13
+ Utils.deprecated("Lumberjack::Device::SizeRollingLogFile", "Lumberjack::Device::SizeRollingLogFile is deprecated and will be removed in version 2.1; use Lumberjack::Device::LogFile instead.")
34
14
 
35
- super
36
- end
15
+ @max_size = options[:max_size]
16
+ new_options = options.reject { |k, _| k == :max_size }.merge(shift_size: max_size)
17
+ new_options[:shift_age] = 10 unless options[:shift_age].is_a?(Integer) && options[:shift_age] >= 0
37
18
 
38
- def archive_file_suffix
39
- next_archive_number.to_s
40
- end
41
-
42
- def roll_file?
43
- @manual || stream.stat.size > @max_size
44
- rescue SystemCallError
45
- false
46
- end
47
-
48
- protected
49
-
50
- # Calculate the next archive file name extension.
51
- def next_archive_number # :nodoc:
52
- max = 0
53
- Dir.glob("#{path}.*").each do |filename|
54
- if /\.\d+\z/ =~ filename
55
- suffix = filename.split(".").last.to_i
56
- max = suffix if suffix > max
57
- end
58
- end
59
- max + 1
60
- end
19
+ super(path, new_options)
61
20
  end
62
21
  end
63
22
  end
@@ -0,0 +1,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # An in-memory logging device designed specifically for testing and debugging
5
+ # scenarios. This device captures log entries in a thread-safe buffer, allowing
6
+ # test code to make assertions about logged content, verify logging behavior,
7
+ # and inspect log entry details without writing to external outputs.
8
+ #
9
+ # The device provides matching capabilities through integration
10
+ # with LogEntryMatcher, supporting pattern matching on messages, severity levels,
11
+ # attributes, and program names. This makes it ideal for comprehensive logging
12
+ # verification in test suites.
13
+ #
14
+ # The buffer is automatically managed with configurable size limits to prevent
15
+ # memory issues during long-running tests, and provides both individual entry
16
+ # access and bulk matching operations.
17
+ #
18
+ # @example Basic test setup
19
+ # logger = Lumberjack::Logger.new(Lumberjack::Device::Test.new)
20
+ # logger.info("User logged in", user_id: 123)
21
+ #
22
+ # expect(logger.device.entries.size).to eq(1)
23
+ # expect(logger.device.last_entry.message).to eq("User logged in")
24
+ #
25
+ # @example Using convenience constructor
26
+ # logger = Lumberjack::Logger.new(:test)
27
+ # logger.warn("Something suspicious", ip: "192.168.1.100")
28
+ #
29
+ # expect(logger.device).to include(severity: :warn, message: /suspicious/)
30
+ # expect(logger.device).to include(attributes: {ip: "192.168.1.100"})
31
+ #
32
+ # @example Advanced pattern matching
33
+ # logger = Lumberjack::Logger.new(:test)
34
+ # logger.error("Database error: connection timeout",
35
+ # database: "users", timeout: 30.5, retry_count: 3)
36
+ #
37
+ # expect(logger.device).to include(
38
+ # severity: :error,
39
+ # message: /Database error/,
40
+ # attributes: {
41
+ # database: "users",
42
+ # timeout: Float,
43
+ # retry_count: be > 0
44
+ # }
45
+ # )
46
+ #
47
+ # @example Nested attribute matching
48
+ # logger.info("Request completed", request: {method: "POST", path: "/users"})
49
+ #
50
+ # expect(logger.device).to include(
51
+ # attributes: {"request.method" => "POST", "request.path" => "/users"}
52
+ # )
53
+ #
54
+ # @example Capturing logs to a file only for failed rspec tests
55
+ # # Set up test logger (presumably in an initializer)
56
+ # Application.logger = Lumberjack::Logger.new(:test)
57
+ #
58
+ # # In your spec_helper or rails_helper.rb
59
+ # RSpec.configure do |config|
60
+ # failed_test_logs = Lumberjack::Logger.new("log/test.log")
61
+ #
62
+ # config.around do |example|
63
+ # Application.logger.device.clear
64
+ #
65
+ # example.run
66
+ #
67
+ # if example.exception
68
+ # failed_test_logs.error("Test failed: #{example.full_description}")
69
+ # Application.logger.device.write_to(failed_test_logs)
70
+ # end
71
+ # end
72
+ # end
73
+ #
74
+ # @see LogEntryMatcher
75
+ class Device::Test < Device
76
+ DeviceRegistry.add(:test, self)
77
+
78
+ # @!attribute [rw] max_entries
79
+ # @return [Integer] The maximum number of entries to retain in the buffer
80
+ attr_accessor :max_entries
81
+
82
+ # Configuration options passed to the constructor. While these don't affect
83
+ # device behavior, they can be useful in tests to verify that options are
84
+ # correctly passed through device creation and configuration pipelines.
85
+ #
86
+ # @return [Hash] A copy of the options hash passed during initialization
87
+ attr_reader :options
88
+
89
+ class << self
90
+ # Format a log entry or expectation hash into a more human readable format. This is
91
+ # intended for use in test failure messages to help diagnose why a match failed when
92
+ # calling +include?+ or +match+.
93
+ #
94
+ # @param expectation [Hash, Lumberjack::LogEntry] The expectation or log entry to format.
95
+ # @option severity [String, Symbol, Integer] The severity level to match.
96
+ # @option message [String, Regexp, Object] Pattern to match against log entry messages.
97
+ # @option attributes [Hash] Hash of attribute patterns to match against log entry attributes.
98
+ # @option progname [String, Regexp, Object] Pattern to match against the program name that generated the log entry.
99
+ # @param indent [Integer] The number of spaces to indent each line.
100
+ # @return [String] A formatted string representation of the expectation or log entry.
101
+ def formatted_expectation(expectation, indent: 0)
102
+ if expectation.is_a?(Lumberjack::LogEntry)
103
+ expectation = {
104
+ "severity" => expectation.severity_label,
105
+ "message" => expectation.message,
106
+ "progname" => expectation.progname,
107
+ "attributes" => expectation.attributes
108
+ }
109
+ end
110
+
111
+ expectation = expectation.transform_keys(&:to_s).compact
112
+ severity = Lumberjack::Severity.coerce(expectation["severity"]) if expectation.include?("severity")
113
+
114
+ message = []
115
+ indent_str = " " * indent
116
+ message << "#{indent_str}severity: #{Lumberjack::Severity.level_to_label(severity)}" if severity
117
+ message << "#{indent_str}message: #{expectation["message"]}" if expectation.include?("message")
118
+ message << "#{indent_str}progname: #{expectation["progname"]}" if expectation.include?("progname")
119
+ if expectation["attributes"].is_a?(Hash) && !expectation["attributes"].empty?
120
+ attributes = Lumberjack::Utils.flatten_attributes(expectation["attributes"])
121
+ label = "attributes:"
122
+ prefix = "#{indent_str}#{label}"
123
+ attributes.sort_by(&:first).each do |name, value|
124
+ message << "#{prefix} #{name}: #{value.inspect}"
125
+ prefix = "#{indent_str}#{" " * label.length}"
126
+ end
127
+ end
128
+ message.join(Lumberjack::LINE_SEPARATOR)
129
+ end
130
+ end
131
+
132
+ # Initialize a new Test device with configurable buffer management.
133
+ # The device creates a thread-safe in-memory buffer for capturing log
134
+ # entries with automatic size management to prevent memory issues.
135
+ #
136
+ # @param options [Hash] Configuration options for the test device
137
+ # @option options [Integer] :max_entries (1000) The maximum number of entries
138
+ # to retain in the buffer. When this limit is exceeded, the oldest entries
139
+ # are automatically removed to maintain the size limit.
140
+ def initialize(options = {})
141
+ @buffer = []
142
+ @max_entries = options[:max_entries] || 1000
143
+ @lock = Mutex.new
144
+ @options = options.dup
145
+ end
146
+
147
+ # Write a log entry to the in-memory buffer. The method is thread-safe and
148
+ # automatically manages buffer size by removing the oldest entries when
149
+ # the maximum capacity is exceeded. Entries are ignored if max_entries is
150
+ # set to less than 1.
151
+ #
152
+ # @param entry [Lumberjack::LogEntry] The log entry to store in the buffer
153
+ # @return [void]
154
+ def write(entry)
155
+ return if max_entries < 1
156
+
157
+ @lock.synchronize do
158
+ @buffer << entry
159
+
160
+ while @buffer.size > max_entries
161
+ @buffer.shift
162
+ end
163
+ end
164
+ end
165
+
166
+ # Return a thread-safe copy of all captured log entries. The returned array
167
+ # is a snapshot of the current buffer state and can be safely modified
168
+ # without affecting the internal buffer.
169
+ #
170
+ # @return [Array<Lumberjack::LogEntry>] A copy of all captured log entries
171
+ # in chronological order (oldest first)
172
+ def entries
173
+ @lock.synchronize { @buffer.dup }
174
+ end
175
+
176
+ # Return the most recently captured log entry. This provides quick access
177
+ # to the latest logged information without needing to access the full
178
+ # entries array.
179
+ #
180
+ # @return [Lumberjack::LogEntry, nil] The most recent log entry, or nil
181
+ # if no entries have been captured yet
182
+ def last_entry
183
+ @buffer.last
184
+ end
185
+
186
+ # Clear all captured log entries from the buffer. This method is useful
187
+ # for resetting the device state between tests or when you want to start
188
+ # fresh log capture without creating a new device instance.
189
+ #
190
+ # @return [void]
191
+ def clear
192
+ @buffer = []
193
+ nil
194
+ end
195
+
196
+ # Write the captured log entries out to another logger or device. This can be useful
197
+ # in testing scenarios where you want to preserve log output for failed tests.
198
+ #
199
+ # @param logger [Lumberjack::Logger, Lumberjack::Device] The target logger or device
200
+ # to which captured entries should be written
201
+ # @return [void]
202
+ def write_to(logger)
203
+ device = (logger.is_a?(Lumberjack::Device) ? logger : logger.device)
204
+ entries.each do |entry|
205
+ device.write(entry)
206
+ end
207
+
208
+ nil
209
+ end
210
+
211
+ # Test whether any captured log entries match the specified criteria.
212
+ # This method provides a convenient interface for making assertions about
213
+ # logged content using flexible pattern matching capabilities.
214
+ #
215
+ # Severity can be specified as a numeric constant (Logger::WARN), symbol
216
+ # (:warn), or string ("warn"). Messages support exact string matching or
217
+ # regular expression patterns. Attributes support nested matching using
218
+ # dot notation and can use any matcher values supported by your test
219
+ # framework (e.g., RSpec's +anything+, +instance_of+, etc.).
220
+ #
221
+ # @param options [Hash] The matching criteria to test against captured entries
222
+ # @option options [String, Regexp, Object] :message Pattern to match against
223
+ # log entry messages. Supports exact strings, regular expressions, or any
224
+ # object that responds to case equality (===)
225
+ # @option options [String, Symbol, Integer] :severity The severity level to
226
+ # match. Accepts symbols (:debug, :info, :warn, :error, :fatal), strings,
227
+ # or numeric Logger constants
228
+ # @option options [Hash] :attributes Hash of attribute patterns to match.
229
+ # Supports nested attributes using dot notation (e.g., "user.id" matches
230
+ # { user: { id: value } }). Values can be exact matches or test framework matchers
231
+ # @option options [String, Regexp, Object] :progname Pattern to match against
232
+ # the program name that generated the log entry
233
+ #
234
+ # @return [Boolean] True if any captured entries match all specified criteria,
235
+ # false otherwise
236
+ #
237
+ # @example Basic message and severity matching
238
+ # expect(device).to include(severity: :error, message: "Database connection failed")
239
+ #
240
+ # @example Regular expression message matching
241
+ # expect(device).to include(severity: :info, message: /User \d+ logged in/)
242
+ #
243
+ # @example Attribute matching with exact values
244
+ # expect(device).to include(attributes: {user_id: 123, action: "login"})
245
+ #
246
+ # @example Nested attribute matching
247
+ # expect(device).to include(attributes: {"request.method" => "POST", "response.status" => 200})
248
+ #
249
+ # @example Using test framework matchers (RSpec example)
250
+ # expect(device).to include(
251
+ # severity: :warn,
252
+ # message: start_with("Warning:"),
253
+ # attributes: {duration: be_a(Float), retries: be > 0}
254
+ # )
255
+ #
256
+ # @example Multiple criteria matching
257
+ # expect(device).to include(
258
+ # severity: :error,
259
+ # message: /timeout/i,
260
+ # progname: "DatabaseWorker",
261
+ # attributes: {database: "users", timeout_seconds: be > 30}
262
+ # )
263
+ def include?(options)
264
+ options = options.transform_keys(&:to_sym)
265
+ !!match(**options)
266
+ end
267
+
268
+ # Find and return the first captured log entry that matches the specified
269
+ # criteria. This method is useful when you need to inspect specific entry
270
+ # details or perform more complex assertions on individual entries.
271
+ #
272
+ # Uses the same flexible matching capabilities as include? but returns
273
+ # the actual LogEntry object instead of a boolean result.
274
+ #
275
+ # @param message [String, Regexp, Object, nil] Pattern to match against
276
+ # log entry messages. Supports exact strings, regular expressions, or
277
+ # any object that responds to case equality (===)
278
+ # @param severity [String, Symbol, Integer, nil] The severity level to match.
279
+ # Accepts symbols, strings, or numeric Logger constants
280
+ # @param attributes [Hash, nil] Hash of attribute patterns to match against
281
+ # log entry attributes. Supports nested matching using dot notation
282
+ # @param progname [String, Regexp, Object, nil] Pattern to match against
283
+ # the program name that generated the log entry
284
+ #
285
+ # @return [Lumberjack::LogEntry, nil] The first matching log entry, or nil
286
+ # if no entries match the specified criteria
287
+ #
288
+ # @example Finding a specific error entry
289
+ # error_entry = device.match(severity: :error, message: /database/i)
290
+ # expect(error_entry.attributes[:table_name]).to eq("users")
291
+ # expect(error_entry.time).to be_within(1.second).of(Time.now)
292
+ #
293
+ # @example Finding entries with specific attributes
294
+ # auth_entry = device.match(attributes: {user_id: 123, action: "login"})
295
+ # expect(auth_entry.severity_label).to eq("INFO")
296
+ # expect(auth_entry.progname).to eq("AuthService")
297
+ #
298
+ # @example Handling no matches
299
+ # missing_entry = device.match(severity: :fatal)
300
+ # expect(missing_entry).to be_nil
301
+ #
302
+ # @example Complex attribute matching
303
+ # api_entry = device.match(
304
+ # message: /API request/,
305
+ # attributes: {"request.endpoint" => "/users", "response.status" => 200}
306
+ # )
307
+ # expect(api_entry.attributes["request.endpoint"]).to eq("/users")
308
+ def match(message: nil, severity: nil, attributes: nil, progname: nil)
309
+ matcher = LogEntryMatcher.new(message: message, severity: severity, attributes: attributes, progname: progname)
310
+ entries.detect { |entry| matcher.match?(entry) }
311
+ end
312
+
313
+ # Get the closest matching log entry from the captured entries based on a scoring system.
314
+ # This method evaluates how well each entry matches the specified criteria and
315
+ # returns the entry with the highest score, provided it meets a minimum threshold.
316
+ # If no entries meet the threshold, nil is returned.
317
+ #
318
+ # This method can be used in tests to return the best match when an assertion fails
319
+ # to aid in diagnosing why no entries met the criteria.
320
+ #
321
+ # @param message [String, Regexp, Object, nil] Pattern to match against
322
+ # log entry messages. Supports exact strings, regular expressions, or
323
+ # any object that responds to case equality (===)
324
+ # @param severity [String, Symbol, Integer, nil] The severity level to match.
325
+ # Accepts symbols, strings, or numeric Logger constants
326
+ # @param attributes [Hash, nil] Hash of attribute patterns to match against
327
+ # log entry attributes. Supports nested matching using dot notation
328
+ # @param progname [String, Regexp, Object, nil] Pattern to match against
329
+ # the program name that generated the log entry
330
+ # @return [Lumberjack::LogEntry, nil] The closest matching log entry, or nil
331
+ # if no entries meet the minimum score threshold
332
+ def closest_match(message: nil, severity: nil, attributes: nil, progname: nil)
333
+ matcher = LogEntryMatcher.new(message: message, severity: severity, attributes: attributes, progname: progname)
334
+ matcher.closest(entries)
335
+ end
336
+ end
337
+ end