Logging in Ruby
Logging in Ruby
Every Ruby application needs a way to record what it is doing. The standard library comes with Logger, a battle-tested class that handles log levels, formatting, and file rotation out of the box. This guide covers everything you need to know to get started.
Creating a Logger
The most common way to create a logger is with Logger.new, passing a filename:
logger = Logger.new("app.log")
logger.info("Application started")
# => I, [2026-03-31T10:00:00.000000 #12345] INFO -- : Application started
logger.close
You can also log to standard output by passing STDOUT:
logger = Logger.new(STDOUT)
logger.info("Printing to stdout")
# => I, [2026-03-31T10:00:00.000000 #12345] INFO -- : Printing to stdout
Or create a logger that writes to a device with a given mode (append, write, or truncate):
# Append to existing file
logger = Logger.new("app.log", "app.log", 10)
# Truncate the file on open
logger = Logger.new("app.log", 0) # '0' means truncate
Log Levels
Logger defines six log levels, from least to most severe:
| Level | Constant | When to use |
|---|---|---|
DEBUG | Logger::DEBUG | Detailed diagnostic information |
INFO | Logger::INFO | General runtime events |
WARN | Logger::WARN | Something unexpected, but not fatal |
ERROR | Logger::ERROR | A serious problem that prevented something |
FATAL | Logger::FATAL | A crash is about to happen |
UNKNOWN | Logger::UNKNOWN | Any message at all |
The default level is INFO. Any messages below the current level are ignored.
logger = Logger.new(STDOUT)
logger.level = Logger::DEBUG
logger.debug("Debug message") # printed
logger.info("Info message") # printed
logger.warn("Warning") # printed
logger.error("Error!") # printed
# => D, [2026-03-31T10:00:00.000000 #12345] DEBUG -- : Debug message
# => I, [2026-03-31T10:00:00.000000 #12345] INFO -- : Info message
# => W, [2026-03-31T10:00:00.000000 #12345] WARN -- : Warning
# => E, [2026-03-31T10:00:00.000000 #12345] ERROR -- : Error!
To silence everything below a certain level:
logger.level = Logger::WARN
logger.debug("Ignored") # not printed
logger.warn("Shown") # printed
Writing Log Messages
The primary methods map directly to log levels:
logger.debug("Something small")
logger.info("Normal event")
logger.warn("Watch out")
logger.error("Something went wrong")
logger.fatal("Dying!")
logger.unknown("This could be anything")
Logger#add
Logger#add (aliased as Logger#log) takes a level as the first argument:
logger.add(Logger::INFO) { "Message inside a block" }
# => I, [2026-03-31T10:00:00.000000 #12345] INFO -- : Message inside a block
Passing a block is efficient because the message string is only built if the log level passes the threshold.
Logger#<<
Logger#<< writes a raw message exactly as given, without adding a severity marker, timestamp, or progname:
logger = Logger.new(STDOUT)
logger << "Plain text without formatting\n"
# => Plain text without formatting
This is useful for dumping raw output directly into the log stream.
Formatting
By default, each log line includes the severity, timestamp, process ID, progname, and the message. You can customise this with Logger#formatter=:
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("User logged in")
# => [2026-03-31 10:00:00] INFO — User logged in
The formatter receives five arguments: severity (string), datetime (Time), progname (string or nil), and the message (typically a msg argument that may be a String or an object).
The progname Argument
The progname is a string that identifies the part of your application generating the log. Set it in the constructor or via Logger#progname=:
logger = Logger.new(STDOUT)
logger.progname = "PaymentService"
logger.info("Charge processed")
# => I, [2026-03-31T10:00:00.000000 #12345] INFO -- PaymentService : Charge processed
You can also override it per-call:
logger.info("EmailService") { "Email sent" }
Custom Datetime Format
To change the timestamp format, pass a datetime_format argument to the constructor:
logger = Logger.new("app.log", datetime_format: "%Y-%m-%d")
logger.info("With custom date format")
# => I, [2026-03-31] INFO -- : With custom date format
Building Custom Loggers
You can subclass Logger to add your own behaviour:
class AppLogger < Logger
def initialize(path)
super(path)
self.level = Logger::INFO
self formatter = ->(severity, datetime, progname, msg) {
"[#{Process.pid}] #{severity} | #{msg}\n"
}
end
def log_request(user_id, action)
info("user_id=#{user_id} action=#{action}")
end
end
logger = AppLogger.new("requests.log")
logger.log_request(42, "create")
# => [12345] INFO | user_id=42 action=create
Or extend an existing logger with refinements or monkey-patches if subclassing feels too heavy.
Log Rotation
Over time log files grow large. Logger supports two rotation strategies.
Size-Based Rotation with Logger::LogDevice
Pass a shift age and shift size to the constructor:
logger = Logger.new("app.log", shift_age: 5, shift_size = 1_048_576)
# Keeps 5 files of up to 1 MB each: app.log, app.log.0, app.log.1, ..., app.log.4
When the file exceeds 1 MB, Logger closes the current file, renames it to app.log.0, and opens a fresh app.log.
Time-Based Rotation
For daily rotation, use Logger::LogDevice directly:
require "logger"
logdev = Logger::LogDevice.new(
"logs/app.log",
shift_age: "daily",
shift_size: 0
)
logger = Logger.new(logdev)
logger.info("Daily rotation active")
Other supported shift_age values include "weekly" and "monthly", or an integer specifying the maximum number of shift files to keep.
Safely Opening a Log File
Always close the logger when you are done, or use a block form that handles it automatically:
Logger.open("app.log") do |log|
log.info("Inside the block")
end
# Logger is closed automatically here
Logging to Files vs STDOUT
Standard output is good for development. Messages appear immediately in your terminal and do not persist:
logger = Logger.new(STDOUT)
logger.warn("Temporary warning")
# Easy to read during development
Files are for production. Logs persist across restarts and can be analysed later:
logger = Logger.new("/var/log/myapp/app.log")
logger.info("Production event")
In production, consider sending logs to STDOUT and letting your deployment system (e.g., Docker, systemd) capture them — this keeps your application decoupled from log file management.
Best Practices
Set the level appropriately per environment. Use DEBUG in development and WARN or ERROR in production to avoid log spam:
case ENV["RACK_ENV"]
when "production" then Logger.new("app.log").level = Logger::WARN
else Logger.new(STDOUT).level = Logger::DEBUG
end
Use blocks for expensive message construction. Ruby’s Logger only evaluates the block if the message will actually be logged:
# Only builds the string if level is DEBUG
logger.debug { "User data: #{expensive_computation}" }
Rotate your logs. Without rotation, disk space will eventually fill up. Use size or time-based rotation from day one.
Do not log sensitive data. Passwords, tokens, and personal information should never appear in logs.
Use a structured format in production. JSON is easy to parse with log aggregation tools:
logger.formatter = proc do |severity, datetime, _progname, msg|
{ severity: severity, time: datetime.iso8601, message: msg.to_s }.to_json + "\n"
end
logger.info("User signed in")
# => {"severity":"INFO","time":"2026-03-31T10:00:00Z","message":"User signed in"}\n
Close your loggers. An unclosed file handle can prevent log rotation or cause data loss on shutdown:
logger = Logger.new("app.log")
logger.info("Done")
logger.close
See Also
- /reference/kernel-methods/warn/ — Kernel#warn, a lightweight alternative to Logger
- /guides/ruby-error-handling/ — How to handle and raise exceptions in Ruby
- /guides/ruby-erb-templates/ — Using ERB templates in Ruby for dynamic content