CrackedRuby logo

CrackedRuby

Logger

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