Ruby Logging — Logger, Log Levels, Formatting and Best Practices Explained
In this tutorial, you will learn about Ruby Logging. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby Logging uses the built-in Logger class for configurable application logging with severity levels, multiple output destinations (file, STDOUT, syslog), and automatic log rotation.
What You'll Learn
- Setting up and configuring Logger
- Using log levels effectively
- Custom log formatting
- Log rotation and management
Why It Matters
Proper logging is essential for debugging and monitoring. Durga Antivirus Pro logs every scan event, threat detection, and configuration change. Doda Browser logs page loads, errors, and performance metrics. Without structured logging, diagnosing production issues becomes guesswork.
Real-World Use
Application error tracking, audit trails, performance monitoring, security event logging, debugging production issues, Compliance logging.
flowchart LR
A["Logging"] --> B["Logger"]
B --> C["Levels"]
C --> D["Formatting"]
D --> E["Rotation"]
E --> F["Best Practices"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
style F fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic Logger Setup
require "logger"
# Log to STDOUT
logger = Logger.new(STDOUT)
logger.info("Application started")
logger.warn("Configuration file not found, using defaults")
logger.error("Failed to connect to database: connection refused")
Log to File
require "logger"
# Log to a file
logger = Logger.new("app.log")
logger.info("Writing to app.log")
# With log rotation — keep 5 files of 10 MB each
logger = Logger.new("app.log", 5, 10_485_760)
# Daily rotation
logger = Logger.new("app.log", "daily")
Log Levels
Logger has six severity levels:
logger = Logger.new(STDOUT)
logger.level = Logger::INFO # Default
logger.debug("Debug message") # Only if level <= DEBUG
logger.info("Info message") # General information
logger.warn("Warning message") # Potential issues
logger.error("Error message") # Recoverable errors
logger.fatal("Fatal message") # Unrecoverable errors
logger.unknown("Unknown level") # Should not happen
Level Filtering
logger = Logger.new(STDOUT)
# Show only warnings and above
logger.level = Logger::WARN
logger.debug("hidden") # Not shown
logger.info("hidden") # Not shown
logger.warn("visible") # Seen
logger.error("visible") # Seen
# Unknown level — always shown
logger.unknown("always") # Seen regardless of level
Log Formatting
Custom Formatter
require "logger"
logger = Logger.new(STDOUT)
logger.formatter = proc do |severity, datetime, progname, msg|
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
end
logger.info("Custom format")
# [2026-06-28 12:30:45] INFO: Custom format
# JSON format for structured logging
logger.formatter = proc do |severity, datetime, progname, msg|
{
timestamp: datetime.utc.iso8601,
level: severity,
message: msg,
pid: Process.pid
}.to_json + "\n"
end
logger.info("JSON log entry")
# {"timestamp":"2026-06-28T12:30:45Z","level":"INFO","message":"JSON log entry","pid":12345}
Multi-line Messages
logger = Logger.new(STDOUT)
# Logger handles multi-line messages
logger.error("First line\nSecond line\nThird line")
# E, [2026-06-28T12:30:45.123456 #12345] ERROR -- : First line
# Second line
# Third line
Advanced Logger Features
Tagged/Progname Logging
logger = Logger.new(STDOUT)
# Tag with program name
logger.info("UserService") { "User #{user_id} logged in" }
logger.error("AuthService") { "Login failed: invalid password" }
# Or set default progname
logger.progname = "MyApp"
logger.info("Default progname applied")
Multiple Output Destinations
require "logger"
require "syslog"
# Create a multi-destination logger
log_file = Logger.new("app.log")
log_console = Logger.new(STDOUT)
class MultiLogger
def initialize(*loggers)
@loggers = loggers
end
def method_missing(name, *args, &block)
@loggers.each { |log| log.send(name, *args, &block) }
end
def respond_to_missing?(name, include_private = false)
@loggers.any? { |log| log.respond_to?(name) } || super
end
end
logger = MultiLogger.new(log_file, log_console)
logger.info("This goes to both file and console")
Logger with Syslog
require "logger"
require "syslog/logger"
syslog = Syslog::Logger.new("myapp")
syslog.info("Logged to syslog")
# /var/log/syslog: Jun 28 12:30:45 hostname myapp: Logged to syslog
Logging Best Practices
Structured Logging
class StructuredLogger
def initialize(logger)
@logger = logger
end
def info(event, metadata = {})
@logger.info(format_event(event, metadata))
end
def error(event, exception = nil, metadata = {})
data = metadata.dup
if exception
data[:exception] = exception.class.to_s
data[:message] = exception.message
data[:backtrace] = exception.backtrace&.first(5)
end
@logger.error(format_event(event, data))
end
private
def format_event(event, metadata)
{
event: event,
timestamp: Time.now.utc.iso8601,
pid: Process.pid
}.merge(metadata).to_json
end
end
logger = StructuredLogger.new(Logger.new(STDOUT))
logger.info("user.login", user_id: 42, ip: "192.168.1.1")
begin
1 / 0
rescue ZeroDivisionError => e
logger.error("calculation.error", e, operation: "division")
end
Contextual Logging
class ContextLogger
def initialize(logger, context = {})
@logger = logger
@context = context
end
def with_context(additional)
self.class.new(@logger, @context.merge(additional))
end
def info(msg)
@logger.info("#{context_prefix}#{msg}")
end
def error(msg)
@logger.error("#{context_prefix}#{msg}")
end
private
def context_prefix
return "" if @context.empty?
"[#{@context.map { |k, v| "#{k}=#{v}" }.join(" ")}] "
end
end
logger = ContextLogger.new(Logger.new(STDOUT))
logger.info("Server started")
# With request context
request_logger = logger.with_context(request_id: "abc-123", user_id: 42)
request_logger.info("Processing order")
# [request_id=abc-123 user_id=42] Processing order
Log Level Management
require "logger"
require "yaml"
class ConfigurableLogger
def initialize(app_name, config_file = "log_config.yml")
@logger = Logger.new("#{app_name}.log")
@logger.level = load_log_level(config_file)
@logger.formatter = default_formatter
end
def load_log_level(config_file)
if File.exist?(config_file)
config = YAML.load_file(config_file)
Logger.const_get(config["log_level"] || "INFO")
else
Logger::INFO
end
rescue NameError
Logger::INFO
end
def default_formatter
proc do |severity, datetime, progname, msg|
"[#{datetime.utc.iso8601}] #{severity.ljust(5)}: #{msg}\n"
end
end
def with_level(level)
old_level = @logger.level
@logger.level = level
yield
ensure
@logger.level = old_level
end
end
logger = ConfigurableLogger.new("myapp")
# Temporarily change level
logger.with_level(Logger::DEBUG) do
logger.debug("Debug info within this block only")
end
logger.debug("This won't show") # Back to configured level
Common Mistakes
1. Logging Sensitive Information
# Bad — logs passwords
logger.info("User #{email} logged in with password #{password}")
# Good — never log credentials
logger.info("User #{email} logged in")
2. Using puts Instead of Logger
# Bad — no level control, no formatting, no rotation
puts "User logged in"
# Good
logger.info("User logged in")
3. Not Setting Log Level in Production
# Bad — debug logs flood production
logger = Logger.new("app.log") # Default level is DEBUG
# Good
logger.level = Logger::INFO # Or ERROR/WARN in production
4. Logging Inside Hot Paths
# Bad — logging every iteration
items.each do |item|
logger.debug("Processing item #{item.id}")
process(item)
end
# Good — batch or sample logging
logger.info("Processing #{items.size} items")
5. Not Handling Log Rotation
# Bad — single file grows unbounded
logger = Logger.new("app.log")
# Good — rotate at 10MB, keep 5 files
logger = Logger.new("app.log", 5, 10_485_760)
Practice Questions
1. What are Ruby Logger's six severity levels?
DEBUG, INFO, WARN, ERROR, FATAL, UNKNOWN. Each level includes all higher levels. Setting level to WARN shows WARN, ERROR, FATAL, and UNKNOWN.
2. How do you configure log rotation?
Pass shift_age and shift_size to the constructor: Logger.new("app.log", 5, 10485760) keeps 5 files of 10 MB each, or use "daily" for daily rotation.
3. How do you customize the log format?
Set a custom logger.formatter proc that accepts severity, datetime, progname, and msg, returning the formatted string.
4. Why not use puts for logging?
puts offers no level control, no timestamp, no formatting, no rotation, no file output, and no structured logging. Logger provides all of these.
Challenge: Create a LeveledLogger class that supports runtime level changes, JSON structured logging, and automatic flushing on crash.
Solution
require "logger"
require "json"
class LeveledLogger
LEVELS = %i[debug info warn error fatal unknown].freeze
def initialize(output = STDOUT, level: :info)
@logger = Logger.new(output)
@logger.level = Logger.const_get(level.upcase)
@logger.formatter = method(:json_formatter)
at_exit { flush }
end
def level=(level)
@logger.level = Logger.const_get(level.to_s.upcase)
end
LEVELS.each do |level|
define_method(level) do |message, metadata = {}|
@logger.send(level) { format_message(message, metadata) }
end
end
private
def json_formatter(severity, datetime, _progname, msg)
{ timestamp: datetime.utc.iso8601, level: severity, message: msg }.to_json + "\n"
end
def format_message(message, metadata)
metadata.empty? ? message : { text: message, **metadata }
end
def flush
@logger.close rescue nil
end
end
log = LeveledLogger.new("app.log", level: :info)
log.info("User created", user_id: 42, source: "signup")
log.error("Database timeout", timeout: 30, retry: 3)
Expected output in log file:
{"timestamp":"2026-06-28T12:30:45Z","level":"INFO","message":{"text":"User created","user_id":42,"source":"signup"}}
{"timestamp":"2026-06-28T12:30:46Z","level":"ERROR","message":{"text":"Database timeout","timeout":30,"retry":3}}
FAQ
{{< faq question="Should I use Logger or a logging gem?" >}} Logger is sufficient for most applications. For advanced needs (structured logging, log aggregation, async logging), consider SemanticLogger, Lograge, or Fluentd. {{< /faq >}}
{{< faq question="What's the best log level for production?" >}} INFO or WARN. DEBUG is too verbose for production. ERROR catches issues needing attention. FATAL indicates unrecoverable failures. {{< /faq >}}
{{< faq question="How do I log to both file and STDOUT?" >}} Create a MultiLogger wrapping both file and STDOUT loggers, or use Logger's log_device parameter to send to multiple destinations. {{< /faq >}}
{{< faq question="Is Logger thread-safe?" >}} Yes. Logger uses a Mutex internally to ensure thread-safe writes. You can safely share a Logger instance across threads. {{< /faq >}}
{{< faq question="How do I anonymize sensitive data in logs?" >}} Create a wrapper that filters sensitive fields (passwords, tokens, PII) before passing to Logger. Use regex or key-based filtering. {{< /faq >}}
Try It Yourself
# logging_demo.rb
require "logger"
require "json"
# Create a JSON logger
logger = Logger.new(STDOUT)
logger.level = Logger::DEBUG
logger.formatter = proc do |severity, datetime, progname, msg|
log_entry = {
time: datetime.strftime("%H:%M:%S"),
level: severity,
msg: msg,
pid: Process.pid
}
log_entry.to_json + "\n"
end
# Simulate application events
logger.debug("Starting debug diagnostics")
logger.info("Application initialized")
logger.warn("Memory usage at 85%")
begin
raise "Simulated error"
rescue => e
logger.error("Error: #{e.message}")
logger.debug("Backtrace: #{e.backtrace.first(3).join(' | ')}")
end
logger.fatal("Unrecoverable: shutting down")
Expected output (formatted):
{"time":"12:30:45","level":"DEBUG","msg":"Starting debug diagnostics","pid":12345}
{"time":"12:30:45","level":"INFO","msg":"Application initialized","pid":12345}
{"time":"12:30:45","level":"WARN","msg":"Memory usage at 85%","pid":12345}
{"time":"12:30:45","level":"ERROR","msg":"Error: Simulated error","pid":12345}
{"time":"12:30:45","level":"FATAL","msg":"Unrecoverable: shutting down","pid":12345}
What's Next
Now that you understand logging, set up Ruby on Rails and build your first web application with the world's most productive web framework.
| Topic | Description | Link |
|---|---|---|
| Ruby Rails Setup | MVC, directory structure | {{< ref "25-rails-setup" >}} |
| Ruby Exception Handling | begin/rescue/ensure | {{< ref "20-exception-handling" >}} |
| Python Logging | Compare Python's logging module | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro