Overview
Ruby's Logger class provides structured logging functionality through the standard library. The Logger creates timestamped messages with severity levels, directing output to files, STDOUT, or custom destinations. Ruby implements Logger as a thread-safe singleton that handles message formatting, level filtering, and output rotation.
The Logger class supports six severity levels: DEBUG, INFO, WARN, ERROR, FATAL, and UNKNOWN. Each logger instance maintains a severity threshold, filtering messages below the configured level. Ruby's Logger handles automatic timestamp generation, message formatting, and concurrent access from multiple threads.
require 'logger'
logger = Logger.new(STDOUT)
logger.info('Application started')
# I, [2024-08-30T10:30:00.123456 #12345] INFO -- : Application started
Logger instances accept various output destinations including file paths, IO objects, and custom writers. The class provides built-in log rotation capabilities, preventing log files from consuming excessive disk space.
# File-based logger with daily rotation
logger = Logger.new('application.log', 'daily')
logger.level = Logger::WARN
logger.warn('Memory usage above threshold')
Ruby's Logger integrates with frameworks like Rails while remaining independent for standalone applications. The class supports custom formatters for specialized output requirements and provides block syntax for conditional message generation.
Basic Usage
Logger instantiation requires an output destination, typically a filename or IO object. Ruby creates the log file if it doesn't exist, establishing write permissions based on the current process.
require 'logger'
# Console logger
console_logger = Logger.new(STDOUT)
# File logger
file_logger = Logger.new('/var/log/myapp.log')
# String IO for testing
string_io = StringIO.new
test_logger = Logger.new(string_io)
The Logger class defines six severity methods corresponding to log levels. Each method accepts a message string or block that generates the message content.
logger = Logger.new(STDOUT)
logger.debug('Variable state: user_id=123')
logger.info('User authentication successful')
logger.warn('API rate limit approaching')
logger.error('Database connection failed')
logger.fatal('Application cannot continue')
logger.unknown('Unclassified system event')
Block syntax defers message generation until the logger determines the message meets the severity threshold. This pattern prevents expensive string operations for filtered messages.
logger.level = Logger::WARN
# String evaluated regardless of level
logger.debug("Expensive operation: #{complex_calculation}")
# Block only evaluated if level permits
logger.debug { "Expensive operation: #{complex_calculation}" }
Logger severity levels create a hierarchy where each level includes all higher severity messages. Setting the level to WARN includes WARN, ERROR, FATAL, and UNKNOWN messages while filtering DEBUG and INFO.
logger = Logger.new(STDOUT)
# Allow INFO and above (INFO, WARN, ERROR, FATAL, UNKNOWN)
logger.level = Logger::INFO
# Allow only ERROR and above (ERROR, FATAL, UNKNOWN)
logger.level = Logger::ERROR
# Check current level
puts logger.debug? # => false when level is ERROR
puts logger.error? # => true when level is ERROR
The Logger supports custom program names that appear in each log message. This feature helps identify message sources in applications with multiple components.
logger = Logger.new(STDOUT)
logger.progname = 'MyApp::UserService'
logger.info('User created successfully')
# I, [2024-08-30T10:30:00.123456 #12345] INFO -- MyApp::UserService: User created successfully
Production Patterns
Production logging requires careful consideration of performance, security, and operational requirements. Ruby's Logger provides several features specifically designed for production environments, including log rotation, custom formatters, and integration patterns.
Log rotation prevents log files from consuming unlimited disk space. Ruby supports size-based and time-based rotation strategies through the Logger constructor parameters.
# Rotate when file exceeds 10MB, keep 5 old files
size_logger = Logger.new('app.log', 5, 10 * 1024 * 1024)
# Daily rotation with automatic cleanup
daily_logger = Logger.new('app.log', 'daily')
# Weekly rotation
weekly_logger = Logger.new('app.log', 'weekly')
# Monthly rotation
monthly_logger = Logger.new('app.log', 'monthly')
Production applications often require structured logging for automated parsing. Custom formatters transform log messages into JSON or other structured formats that log aggregation systems can process.
class JSONFormatter < Logger::Formatter
def call(severity, time, progname, msg)
{
timestamp: time.iso8601,
level: severity,
program: progname,
message: msg,
hostname: Socket.gethostname,
pid: Process.pid
}.to_json + "\n"
end
end
logger = Logger.new(STDOUT)
logger.formatter = JSONFormatter.new
logger.info('User login successful', { user_id: 123, ip: '192.168.1.100' })
Multi-tier applications benefit from contextual logging that includes request IDs, user information, and transaction boundaries. Ruby's Logger supports this through custom formatter implementations and structured message patterns.
class ContextualLogger
def initialize(base_logger)
@logger = base_logger
@context = {}
end
def with_context(**context)
old_context = @context
@context = @context.merge(context)
yield
ensure
@context = old_context
end
def info(message)
@logger.info("#{@context.inspect} #{message}")
end
def error(message)
@logger.error("#{@context.inspect} #{message}")
end
end
logger = ContextualLogger.new(Logger.new(STDOUT))
logger.with_context(request_id: 'req-123', user_id: 456) do
logger.info('Processing user update')
logger.error('Validation failed')
end
Production environments often require multiple log destinations for different purposes. Ruby applications can implement log broadcasting to send messages to multiple loggers simultaneously.
class BroadcastLogger
def initialize(*loggers)
@loggers = loggers
end
[:debug, :info, :warn, :error, :fatal].each do |level|
define_method(level) do |message = nil, &block|
@loggers.each { |logger| logger.send(level, message, &block) }
end
end
end
console_logger = Logger.new(STDOUT)
file_logger = Logger.new('/var/log/app.log')
error_logger = Logger.new('/var/log/errors.log')
error_logger.level = Logger::ERROR
broadcast = BroadcastLogger.new(console_logger, file_logger, error_logger)
broadcast.info('Application event') # Goes to console and file
broadcast.error('Critical error') # Goes to all three loggers
Performance & Memory
Logger performance directly impacts application throughput, especially in high-traffic scenarios. Ruby's Logger implementation includes several optimizations, but understanding performance characteristics helps prevent bottlenecks.
Block-based message generation provides the most significant performance improvement for filtered messages. When the logger level filters out messages, block syntax prevents expensive string interpolation and object creation.
require 'benchmark'
logger = Logger.new('/dev/null')
logger.level = Logger::WARN
# Performance test data
users = Array.new(10000) { { id: rand(1000), name: "User#{rand(1000)}" } }
# Expensive string interpolation always executes
time_string = Benchmark.measure do
users.each do |user|
logger.debug("Processing user: #{user[:name]} (ID: #{user[:id]})")
end
end
# Block syntax skips evaluation when level filters message
time_block = Benchmark.measure do
users.each do |user|
logger.debug { "Processing user: #{user[:name]} (ID: #{user[:id]})" }
end
end
puts "String interpolation: #{time_string.real}s"
puts "Block syntax: #{time_block.real}s"
# Block syntax typically 10-50x faster when messages are filtered
Logger writes create I/O operations that can become bottlenecks. Asynchronous logging patterns help maintain application performance while preserving log message ordering.
require 'thread'
class AsyncLogger
def initialize(logger)
@logger = logger
@queue = Queue.new
@worker = Thread.new { process_messages }
end
def info(message)
@queue << [:info, message, Time.now]
end
def error(message)
@queue << [:error, message, Time.now]
end
def shutdown
@queue << :shutdown
@worker.join
end
private
def process_messages
loop do
item = @queue.pop
break if item == :shutdown
level, message, timestamp = item
@logger.send(level) { "[#{timestamp}] #{message}" }
end
end
end
# Usage maintains normal Logger interface
async_logger = AsyncLogger.new(Logger.new('app.log'))
async_logger.info('Non-blocking log message')
async_logger.shutdown # Clean shutdown flushes remaining messages
Memory usage becomes significant in applications that generate large numbers of log messages. Logger instances maintain internal buffers and formatters that accumulate memory over time. Regular log rotation and proper logger lifecycle management prevent memory leaks.
# Memory-conscious logger configuration
logger = Logger.new('app.log', 10, 50 * 1024 * 1024) # 50MB rotation
logger.level = Logger::INFO # Filter debug messages
# Custom formatter with minimal memory allocation
class MinimalFormatter < Logger::Formatter
def call(severity, time, progname, msg)
"#{time.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
end
end
logger.formatter = MinimalFormatter.new
Log message frequency analysis helps identify performance hotspots. Applications should implement log sampling for high-frequency debug messages to prevent overwhelming the logging system.
class SampledLogger
def initialize(logger, sample_rate = 0.1)
@logger = logger
@sample_rate = sample_rate
end
def debug(message)
return unless rand < @sample_rate
@logger.debug(message)
end
def info(message)
@logger.info(message)
end
def error(message)
@logger.error(message)
end
end
# Only logs 10% of debug messages
sampled = SampledLogger.new(Logger.new(STDOUT), 0.1)
1000.times { |i| sampled.debug("Debug message #{i}") } # ~100 messages logged
Error Handling & Debugging
Logger operations can fail due to filesystem issues, permission problems, or network connectivity when using remote logging. Ruby's Logger raises specific exceptions that applications should handle appropriately.
File permission errors occur when the Logger cannot write to the specified destination. These exceptions appear during Logger instantiation or when log rotation creates new files.
begin
logger = Logger.new('/etc/secure.log') # May require elevated permissions
rescue Errno::EACCES => e
puts "Permission denied: #{e.message}"
# Fallback to console logging
logger = Logger.new(STDOUT)
rescue Errno::ENOENT => e
puts "Directory not found: #{e.message}"
# Create directory or use alternative path
Dir.mkdir('/var/log/myapp') rescue nil
logger = Logger.new('/var/log/myapp/app.log')
end
Disk space exhaustion prevents Logger from writing messages, potentially causing application failures. Monitoring disk usage and implementing fallback strategies prevents silent logging failures.
class RobustLogger
def initialize(primary_path, fallback_logger = nil)
@primary_logger = Logger.new(primary_path)
@fallback_logger = fallback_logger || Logger.new(STDERR)
@failed = false
end
def info(message)
return @fallback_logger.info(message) if @failed
begin
@primary_logger.info(message)
rescue Errno::ENOSPC => e
@failed = true
@fallback_logger.error("Primary logger failed (disk full), switching to fallback")
@fallback_logger.info(message)
rescue StandardError => e
@fallback_logger.error("Logging error: #{e.message}")
@fallback_logger.info(message)
end
end
def error(message)
# Always attempt to log errors to both destinations
[@primary_logger, @fallback_logger].each do |logger|
begin
logger.error(message) if logger
rescue StandardError
# Silent failure for fallback errors
end
end
end
end
logger = RobustLogger.new('/var/log/app.log')
logger.info('This message has fallback protection')
Log rotation can fail when old log files are locked by other processes or when filesystem operations encounter errors. These failures require careful handling to prevent logging interruption.
class SafeRotatingLogger
def initialize(base_path, max_files = 5, max_size = 10 * 1024 * 1024)
@base_path = base_path
@max_files = max_files
@max_size = max_size
@current_logger = create_logger
end
def info(message)
check_rotation
@current_logger.info(message)
end
private
def create_logger
Logger.new(@base_path, @max_files, @max_size)
rescue StandardError => e
Logger.new(STDERR).tap do |fallback|
fallback.error("Failed to create primary logger: #{e.message}")
end
end
def check_rotation
return unless File.exist?(@base_path)
return unless File.size(@base_path) > @max_size
begin
# Force rotation by creating new logger
@current_logger = create_logger
rescue StandardError => e
@current_logger.error("Rotation failed: #{e.message}")
end
end
end
Debug logging can overwhelm systems during troubleshooting. Implementing level-specific debugging helps isolate issues without impacting overall application performance.
class DebugLogger
def initialize(base_logger)
@base_logger = base_logger
@debug_patterns = []
@original_level = base_logger.level
end
def enable_debug_for(*patterns)
@debug_patterns.concat(patterns)
@base_logger.level = Logger::DEBUG if @debug_patterns.any?
end
def disable_debug
@debug_patterns.clear
@base_logger.level = @original_level
end
def debug(message)
return unless @debug_patterns.any? { |pattern| message.match?(pattern) }
@base_logger.debug(message)
end
def info(message); @base_logger.info(message); end
def error(message); @base_logger.error(message); end
end
debug_logger = DebugLogger.new(Logger.new(STDOUT))
debug_logger.enable_debug_for(/user/, /database/)
debug_logger.debug('User authentication started') # Logged
debug_logger.debug('Cache miss occurred') # Not logged
debug_logger.info('Application started') # Always logged
Thread Safety & Concurrency
Ruby's Logger class provides thread-safe logging operations, using internal synchronization to prevent message interleaving. Multiple threads can safely write to the same Logger instance without external synchronization.
require 'thread'
logger = Logger.new(STDOUT)
# Multiple threads writing concurrently
threads = 10.times.map do |i|
Thread.new do
100.times do |j|
logger.info("Thread #{i}, Message #{j}")
sleep(0.001) # Simulate work
end
end
end
threads.each(&:join)
# All messages appear complete and properly formatted
Logger thread safety extends to log rotation operations. When one thread triggers rotation, other threads block until the rotation completes, ensuring consistent file handles across all logging operations.
# Thread-safe rotation demonstration
logger = Logger.new('concurrent.log', 3, 1024) # Small size for frequent rotation
producers = 20.times.map do |producer_id|
Thread.new do
500.times do |message_id|
logger.info("Producer #{producer_id}: Message #{message_id} - #{'x' * 100}")
end
end
end
producers.each(&:join)
# Log files rotate cleanly without corruption
Applications using Logger in multi-threaded environments should consider thread-local context for request tracking. Thread-local storage provides isolation while maintaining Logger thread safety.
class ThreadContextLogger
def initialize(base_logger)
@base_logger = base_logger
end
def with_context(**context)
Thread.current[:log_context] = context
yield
ensure
Thread.current[:log_context] = nil
end
def info(message)
context = Thread.current[:log_context] || {}
formatted_message = context.empty? ? message : "#{context.inspect} #{message}"
@base_logger.info(formatted_message)
end
def error(message)
context = Thread.current[:log_context] || {}
formatted_message = context.empty? ? message : "#{context.inspect} #{message}"
@base_logger.error(formatted_message)
end
end
context_logger = ThreadContextLogger.new(Logger.new(STDOUT))
# Each thread maintains independent context
request_threads = 5.times.map do |request_id|
Thread.new do
context_logger.with_context(request_id: "req-#{request_id}", user_id: request_id * 100) do
context_logger.info('Processing request')
sleep(0.1)
context_logger.info('Request completed')
end
end
end
request_threads.each(&:join)
Concurrent applications often require coordination between logging and other thread-safe operations. Logger synchronization integrates cleanly with other Ruby synchronization primitives.
class CoordinatedLogger
def initialize(base_logger)
@base_logger = base_logger
@mutex = Mutex.new
@condition = ConditionVariable.new
@shutdown = false
end
def info(message)
@mutex.synchronize do
return if @shutdown
@base_logger.info(message)
end
end
def shutdown_and_flush
@mutex.synchronize do
@shutdown = true
@base_logger.info('Logger shutdown initiated')
@condition.broadcast
end
end
def wait_for_shutdown
@mutex.synchronize do
@condition.wait(@mutex) until @shutdown
end
end
end
coordinated = CoordinatedLogger.new(Logger.new('coordinated.log'))
# Background logging thread
logging_thread = Thread.new do
100.times { |i| coordinated.info("Background message #{i}"); sleep(0.01) }
end
# Shutdown coordination
shutdown_thread = Thread.new do
sleep(0.5)
coordinated.shutdown_and_flush
end
logging_thread.join
shutdown_thread.join
Reference
Logger Class Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Logger.new(logdev, shift_age = 0, shift_size = 1048576) |
logdev (String/IO), shift_age (Integer/String), shift_size (Integer) |
Logger |
Creates new Logger instance with optional rotation |
Instance Methods - Logging
Method | Parameters | Returns | Description |
---|---|---|---|
#debug(message = nil, &block) |
message (String), block |
nil |
Logs DEBUG level message |
#info(message = nil, &block) |
message (String), block |
nil |
Logs INFO level message |
#warn(message = nil, &block) |
message (String), block |
nil |
Logs WARN level message |
#error(message = nil, &block) |
message (String), block |
nil |
Logs ERROR level message |
#fatal(message = nil, &block) |
message (String), block |
nil |
Logs FATAL level message |
#unknown(message = nil, &block) |
message (String), block |
nil |
Logs UNKNOWN level message |
Instance Methods - Level Checking
Method | Parameters | Returns | Description |
---|---|---|---|
#debug? |
None | Boolean |
Returns true if DEBUG level enabled |
#info? |
None | Boolean |
Returns true if INFO level enabled |
#warn? |
None | Boolean |
Returns true if WARN level enabled |
#error? |
None | Boolean |
Returns true if ERROR level enabled |
#fatal? |
None | Boolean |
Returns true if FATAL level enabled |
Instance Methods - Configuration
Method | Parameters | Returns | Description |
---|---|---|---|
#level |
None | Integer |
Returns current log level |
#level=(level) |
level (Integer/Symbol) |
Integer |
Sets log level threshold |
#progname |
None | String |
Returns program name |
#progname=(name) |
name (String) |
String |
Sets program name for messages |
#formatter |
None | Logger::Formatter |
Returns message formatter |
#formatter=(formatter) |
formatter (Formatter) |
Formatter |
Sets custom message formatter |
Severity Level Constants
Constant | Value | Description |
---|---|---|
Logger::DEBUG |
0 | Detailed debugging information |
Logger::INFO |
1 | General informational messages |
Logger::WARN |
2 | Warning conditions |
Logger::ERROR |
3 | Error conditions |
Logger::FATAL |
4 | Fatal error conditions |
Logger::UNKNOWN |
5 | Unknown severity level |
Log Rotation Options
shift_age Value | Description |
---|---|
Integer | Number of old log files to retain |
'daily' |
Rotate daily at midnight |
'weekly' |
Rotate weekly on Sunday |
'monthly' |
Rotate monthly on first day |
Default Message Format
severity, datetime, progname, message
Example output:
I, [2024-08-30T10:30:00.123456 #12345] INFO -- MyApp: User login successful
Formatter Interface
Custom formatters must implement the call
method:
def call(severity, time, progname, msg)
# Return formatted string
end
Thread Safety Guarantees
- Logger instances are thread-safe for concurrent access
- Log rotation operations are synchronized across threads
- Message formatting and writing are atomic operations
- Multiple threads can share single Logger instance safely
Exception Hierarchy
Exception | Condition |
---|---|
Errno::EACCES |
Permission denied for log file |
Errno::ENOENT |
Log directory does not exist |
Errno::ENOSPC |
Insufficient disk space |
IOError |
General I/O error during logging |