Class: Lumberjack::Device::Test

Inherits:
Device
  • Object
show all
Defined in:
lib/lumberjack/device/test.rb

Overview

An in-memory logging device designed specifically for testing and debugging scenarios. This device captures log entries in a thread-safe buffer, allowing test code to make assertions about logged content, verify logging behavior, and inspect log entry details without writing to external outputs.

The device provides matching capabilities through integration with LogEntryMatcher, supporting pattern matching on messages, severity levels, attributes, and program names. This makes it ideal for comprehensive logging verification in test suites.

The buffer is automatically managed with configurable size limits to prevent memory issues during long-running tests, and provides both individual entry access and bulk matching operations.

Examples:

Basic test setup

logger = Lumberjack::Logger.new(Lumberjack::Device::Test.new)
logger.info("User logged in", user_id: 123)

expect(logger.device.entries.size).to eq(1)
expect(logger.device.last_entry.message).to eq("User logged in")

Using convenience constructor

logger = Lumberjack::Logger.new(:test)
logger.warn("Something suspicious", ip: "192.168.1.100")

expect(logger.device).to include(severity: :warn, message: /suspicious/)
expect(logger.device).to include(attributes: {ip: "192.168.1.100"})

Advanced pattern matching

logger = Lumberjack::Logger.new(:test)
logger.error("Database error: connection timeout",
             database: "users", timeout: 30.5, retry_count: 3)

expect(logger.device).to include(
  severity: :error,
  message: /Database error/,
  attributes: {
    database: "users",
    timeout: Float,
    retry_count: be > 0
  }
)

Nested attribute matching

logger.info("Request completed", request: {method: "POST", path: "/users"})

expect(logger.device).to include(
  attributes: {"request.method" => "POST", "request.path" => "/users"}
)

Capturing logs to a file only for failed rspec tests

# Set up test logger (presumably in an initializer)
Application.logger = Lumberjack::Logger.new(:test)

# In your spec_helper or rails_helper.rb
RSpec.configure do |config|
  failed_test_logs = Lumberjack::Logger.new("log/test.log")

  config.around do |example|
    Application.logger.device.clear

    example.run

    if example.exception
      failed_test_logs.error("Test failed: #{example.full_description}")
      Application.logger.device.write_to(failed_test_logs)
    end
  end
end

See Also:

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Test

Initialize a new Test device with configurable buffer management. The device creates a thread-safe in-memory buffer for capturing log entries with automatic size management to prevent memory issues.

Parameters:

  • options (Hash) (defaults to: {})

    Configuration options for the test device

Options Hash (options):

  • :max_entries (Integer) — default: 1000

    The maximum number of entries to retain in the buffer. When this limit is exceeded, the oldest entries are automatically removed to maintain the size limit.

  • :entry_formatter (Lumberjack::EntryFormatter)

    Entry formatter used when matching entries. See #entry_formatter.



175
176
177
178
179
180
181
# File 'lib/lumberjack/device/test.rb', line 175

def initialize(options = {})
  @buffer = []
  @max_entries = options[:max_entries] || 1000
  @entry_formatter = options[:entry_formatter]
  @lock = Mutex.new
  @options = options.dup
end

Instance Attribute Details

#entry_formatterLumberjack::EntryFormatter?

Entry formatter used by include?, match, and closest_match when building matchers. Log entries are captured after the logger's formatter has been applied, so setting this to the same formatter used by the logger allows expectations to be written with unformatted values:

logger.device.entry_formatter = logger.formatter

Returns:



98
99
100
# File 'lib/lumberjack/device/test.rb', line 98

def entry_formatter
  @entry_formatter
end

#max_entriesInteger

Returns The maximum number of entries to retain in the buffer.

Returns:

  • (Integer)

    The maximum number of entries to retain in the buffer



80
81
82
# File 'lib/lumberjack/device/test.rb', line 80

def max_entries
  @max_entries
end

#optionsHash (readonly)

Configuration options passed to the constructor. While these don't affect device behavior, they can be useful in tests to verify that options are correctly passed through device creation and configuration pipelines.

Returns:

  • (Hash)

    A copy of the options hash passed during initialization



87
88
89
# File 'lib/lumberjack/device/test.rb', line 87

def options
  @options
end

Class Method Details

.formatted_expectation(expectation, indent: 0) ⇒ String

Format a log entry or expectation hash into a more human readable format. This is intended for use in test failure messages to help diagnose why a match failed when calling include? or match.

Parameters:

  • expectation (Hash, Lumberjack::LogEntry)

    The expectation or log entry to format.

  • indent (Integer) (defaults to: 0)

    The number of spaces to indent each line.

  • severity (Hash)

    a customizable set of options

  • message (Hash)

    a customizable set of options

  • attributes (Hash)

    a customizable set of options

  • progname (Hash)

    a customizable set of options

Returns:

  • (String)

    A formatted string representation of the expectation or log entry.



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/lumberjack/device/test.rb', line 112

def formatted_expectation(expectation, indent: 0)
  if expectation.is_a?(Lumberjack::LogEntry)
    expectation = {
      "severity" => expectation.severity_label,
      "message" => expectation.message,
      "progname" => expectation.progname,
      "attributes" => expectation.attributes
    }
  end

  expectation = expectation.transform_keys(&:to_s).compact
  severity = Lumberjack::Severity.coerce(expectation["severity"]) if expectation.include?("severity")

  message = []
  indent_str = " " * indent
  message << "#{indent_str}severity: #{Lumberjack::Severity.level_to_label(severity)}" if severity
  message << "#{indent_str}message: #{expectation["message"]}" if expectation.include?("message")
  message << "#{indent_str}progname: #{expectation["progname"]}" if expectation.include?("progname")
  expected_attributes = expectation["attributes"]
  if expected_attributes.is_a?(Hash)
    unless expected_attributes.empty?
      attributes = Lumberjack::Utils.flatten_attributes(expected_attributes)
      label = "attributes:"
      prefix = "#{indent_str}#{label}"
      attributes.sort_by(&:first).each do |name, value|
        message << "#{prefix} #{name}: #{formatted_value(value)}"
        prefix = "#{indent_str}#{" " * label.length}"
      end
    end
  elsif expected_attributes
    # Matchers like RSpec's hash_including are matched against the attributes hash as a whole.
    message << "#{indent_str}attributes: #{formatted_value(expected_attributes)}"
  end
  message.join(Lumberjack::LINE_SEPARATOR)
end

Instance Method Details

#clearvoid

This method returns an undefined value.

Clear all captured log entries from the buffer. This method is useful for resetting the device state between tests or when you want to start fresh log capture without creating a new device instance.



227
228
229
230
# File 'lib/lumberjack/device/test.rb', line 227

def clear
  @lock.synchronize { @buffer = [] }
  nil
end

#closest_match(message: nil, severity: nil, attributes: nil, progname: nil, formatter: nil) ⇒ Lumberjack::LogEntry?

Get the closest matching log entry from the captured entries based on a scoring system. This method evaluates how well each entry matches the specified criteria and returns the entry with the highest score, provided it meets a minimum threshold. If no entries meet the threshold, nil is returned.

This method can be used in tests to return the best match when an assertion fails to aid in diagnosing why no entries met the criteria.

Parameters:

  • message (String, Regexp, Object, nil) (defaults to: nil)

    Pattern to match against log entry messages. Supports exact strings, regular expressions, or any object that responds to case equality (===)

  • severity (String, Symbol, Integer, nil) (defaults to: nil)

    The severity level to match. Accepts symbols, strings, or numeric Logger constants

  • attributes (Hash, nil) (defaults to: nil)

    Hash of attribute patterns to match against log entry attributes. Supports nested matching using dot notation

  • progname (String, Regexp, Object, nil) (defaults to: nil)

    Pattern to match against the program name that generated the log entry

  • formatter (Lumberjack::EntryFormatter, Lumberjack::Logger, nil) (defaults to: nil)

    Formatter used to format filter values when a raw comparison fails. Defaults to the device #entry_formatter.

Returns:

  • (Lumberjack::LogEntry, nil)

    The closest matching log entry, or nil if no entries meet the minimum score threshold



383
384
385
386
387
388
389
390
391
392
# File 'lib/lumberjack/device/test.rb', line 383

def closest_match(message: nil, severity: nil, attributes: nil, progname: nil, formatter: nil)
  matcher = LogEntryMatcher.new(
    message: message,
    severity: severity,
    attributes: attributes,
    progname: progname,
    formatter: formatter || entry_formatter
  )
  matcher.closest(entries)
end

#entriesArray<Lumberjack::LogEntry>

Return a thread-safe copy of all captured log entries. The returned array is a snapshot of the current buffer state and can be safely modified without affecting the internal buffer.

Returns:

  • (Array<Lumberjack::LogEntry>)

    A copy of all captured log entries in chronological order (oldest first)



208
209
210
# File 'lib/lumberjack/device/test.rb', line 208

def entries
  @lock.synchronize { @buffer.dup }
end

#include?(options) ⇒ Boolean

Test whether any captured log entries match the specified criteria. This method provides a convenient interface for making assertions about logged content using flexible pattern matching capabilities.

Severity can be specified as a numeric constant (Logger::WARN), symbol (:warn), or string ("warn"). Messages support exact string matching or regular expression patterns. Attributes support nested matching using dot notation and can use any matcher values supported by your test framework (e.g., RSpec's anything, instance_of, etc.).

Examples:

Basic message and severity matching

expect(device).to include(severity: :error, message: "Database connection failed")

Regular expression message matching

expect(device).to include(severity: :info, message: /User \d+ logged in/)

Attribute matching with exact values

expect(device).to include(attributes: {user_id: 123, action: "login"})

Nested attribute matching

expect(device).to include(attributes: {"request.method" => "POST", "response.status" => 200})

Using test framework matchers (RSpec example)

expect(device).to include(
  severity: :warn,
  message: start_with("Warning:"),
  attributes: {duration: be_a(Float), retries: be > 0}
)

Multiple criteria matching

expect(device).to include(
  severity: :error,
  message: /timeout/i,
  progname: "DatabaseWorker",
  attributes: {database: "users", timeout_seconds: be > 30}
)

Parameters:

  • options (Hash)

    The matching criteria to test against captured entries

Options Hash (options):

  • :message (String, Regexp, Object)

    Pattern to match against log entry messages. Supports exact strings, regular expressions, or any object that responds to case equality (===)

  • :severity (String, Symbol, Integer)

    The severity level to match. Accepts symbols (:debug, :info, :warn, :error, :fatal), strings, or numeric Logger constants

  • :attributes (Hash)

    Hash of attribute patterns to match. Supports nested attributes using dot notation (e.g., "user.id" matches { user: { id: value } }). Values can be exact matches or test framework matchers

  • :progname (String, Regexp, Object)

    Pattern to match against the program name that generated the log entry

  • :formatter (Lumberjack::EntryFormatter, Lumberjack::Logger)

    Formatter used to format filter values when a raw comparison fails. Defaults to the device #entry_formatter.

Returns:

  • (Boolean)

    True if any captured entries match all specified criteria, false otherwise



302
303
304
305
# File 'lib/lumberjack/device/test.rb', line 302

def include?(options)
  options = options.transform_keys(&:to_sym)
  !!match(**options)
end

#last_entryLumberjack::LogEntry?

Return the most recently captured log entry. This provides quick access to the latest logged information without needing to access the full entries array.

Returns:

  • (Lumberjack::LogEntry, nil)

    The most recent log entry, or nil if no entries have been captured yet



218
219
220
# File 'lib/lumberjack/device/test.rb', line 218

def last_entry
  @lock.synchronize { @buffer.last }
end

#match(message: nil, severity: nil, attributes: nil, progname: nil, formatter: nil) ⇒ Lumberjack::LogEntry?

Find and return the first captured log entry that matches the specified criteria. This method is useful when you need to inspect specific entry details or perform more complex assertions on individual entries.

Uses the same flexible matching capabilities as include? but returns the actual LogEntry object instead of a boolean result.

Examples:

Finding a specific error entry

error_entry = device.match(severity: :error, message: /database/i)
expect(error_entry.attributes[:table_name]).to eq("users")
expect(error_entry.time).to be_within(1.second).of(Time.now)

Finding entries with specific attributes

auth_entry = device.match(attributes: {user_id: 123, action: "login"})
expect(auth_entry.severity_label).to eq("INFO")
expect(auth_entry.progname).to eq("AuthService")

Handling no matches

missing_entry = device.match(severity: :fatal)
expect(missing_entry).to be_nil

Complex attribute matching

api_entry = device.match(
  message: /API request/,
  attributes: {"request.endpoint" => "/users", "response.status" => 200}
)
expect(api_entry.attributes["request.endpoint"]).to eq("/users")

Parameters:

  • message (String, Regexp, Object, nil) (defaults to: nil)

    Pattern to match against log entry messages. Supports exact strings, regular expressions, or any object that responds to case equality (===)

  • severity (String, Symbol, Integer, nil) (defaults to: nil)

    The severity level to match. Accepts symbols, strings, or numeric Logger constants

  • attributes (Hash, nil) (defaults to: nil)

    Hash of attribute patterns to match against log entry attributes. Supports nested matching using dot notation

  • progname (String, Regexp, Object, nil) (defaults to: nil)

    Pattern to match against the program name that generated the log entry

  • formatter (Lumberjack::EntryFormatter, Lumberjack::Logger, nil) (defaults to: nil)

    Formatter used to format filter values when a raw comparison fails. Defaults to the device #entry_formatter.

Returns:

  • (Lumberjack::LogEntry, nil)

    The first matching log entry, or nil if no entries match the specified criteria



350
351
352
353
354
355
356
357
358
359
# File 'lib/lumberjack/device/test.rb', line 350

def match(message: nil, severity: nil, attributes: nil, progname: nil, formatter: nil)
  matcher = LogEntryMatcher.new(
    message: message,
    severity: severity,
    attributes: attributes,
    progname: progname,
    formatter: formatter || entry_formatter
  )
  entries.detect { |entry| matcher.match?(entry) }
end

#write(entry) ⇒ void

This method returns an undefined value.

Write a log entry to the in-memory buffer. The method is thread-safe and automatically manages buffer size by removing the oldest entries when the maximum capacity is exceeded. Entries are ignored if max_entries is set to less than 1.

Parameters:



190
191
192
193
194
195
196
197
198
199
200
# File 'lib/lumberjack/device/test.rb', line 190

def write(entry)
  return if max_entries < 1

  @lock.synchronize do
    @buffer << entry

    while @buffer.size > max_entries
      @buffer.shift
    end
  end
end

#write_to(logger) ⇒ void

This method returns an undefined value.

Write the captured log entries out to another logger or device. This can be useful in testing scenarios where you want to preserve log output for failed tests.

Parameters:



238
239
240
241
242
243
244
245
# File 'lib/lumberjack/device/test.rb', line 238

def write_to(logger)
  device = (logger.is_a?(Lumberjack::Device) ? logger : logger.device)
  entries.each do |entry|
    device.write(entry)
  end

  nil
end