Class: Lumberjack::Device::Test

Inherits:
Lumberjack::Device 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

Methods inherited from Lumberjack::Device

#close, #datetime_format, #datetime_format=, #dev, #flush, open_device, #reopen

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.



140
141
142
143
144
145
# File 'lib/lumberjack/device/test.rb', line 140

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

Instance Attribute Details

#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.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/lumberjack/device/test.rb', line 101

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")
  if expectation["attributes"].is_a?(Hash) && !expectation["attributes"].empty?
    attributes = Lumberjack::Utils.flatten_attributes(expectation["attributes"])
    label = "attributes:"
    prefix = "#{indent_str}#{label}"
    attributes.sort_by(&:first).each do |name, value|
      message << "#{prefix} #{name}: #{value.inspect}"
      prefix = "#{indent_str}#{" " * label.length}"
    end
  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.



191
192
193
194
# File 'lib/lumberjack/device/test.rb', line 191

def clear
  @buffer = []
  nil
end

#closest_match(message: nil, severity: nil, attributes: nil, progname: 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

Returns:

  • (Lumberjack::LogEntry, nil)

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



332
333
334
335
# File 'lib/lumberjack/device/test.rb', line 332

def closest_match(message: nil, severity: nil, attributes: nil, progname: nil)
  matcher = LogEntryMatcher.new(message: message, severity: severity, attributes: attributes, progname: progname)
  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)



172
173
174
# File 'lib/lumberjack/device/test.rb', line 172

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

Returns:

  • (Boolean)

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



263
264
265
266
# File 'lib/lumberjack/device/test.rb', line 263

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



182
183
184
# File 'lib/lumberjack/device/test.rb', line 182

def last_entry
  @buffer.last
end

#match(message: nil, severity: nil, attributes: nil, progname: 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

Returns:

  • (Lumberjack::LogEntry, nil)

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



308
309
310
311
# File 'lib/lumberjack/device/test.rb', line 308

def match(message: nil, severity: nil, attributes: nil, progname: nil)
  matcher = LogEntryMatcher.new(message: message, severity: severity, attributes: attributes, progname: progname)
  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:



154
155
156
157
158
159
160
161
162
163
164
# File 'lib/lumberjack/device/test.rb', line 154

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:



202
203
204
205
206
207
208
209
# File 'lib/lumberjack/device/test.rb', line 202

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

  nil
end