Backends

Backends are responsible for writing log entries to specific destinations. dry-logger supports multiple backends, allowing you to log to several destinations simultaneously with different configurations for each.

Understanding backends

A backend combines:

  • An output destination (stdout, file, external logger, etc.)
  • A formatter (how entries are formatted)
  • Optional conditional logging (when to log)

Multiple backends

Adding backends

Add backends to the default logger:

logger = Dry.Logger(:my_app)
  .add_backend(stream: "logs/application.log")
  .add_backend(stream: "logs/errors.log", log_if: :error?)

logger.info("User logged in")
# Goes to stdout and logs/application.log

logger.error("Database connection failed")
# Goes to stdout, logs/application.log, and logs/errors.log

Block-based configuration

For more control, configure all backends in a block:

logger = Dry.Logger(:my_app) do |setup|
  setup.add_backend(stream: "logs/application.log", template: :details)
  setup.add_backend(stream: "logs/json.log", formatter: :json)
end

# Only logs to the files you configured (no stdout)

When you use a block, dry-logger skips creating the default stdout backend, giving you complete control.

Conditional logging

The log_if option controls when a backend should log an entry. This is useful for routing different log levels to different destinations.

Using severity methods

Filter by log level using the entry's severity methods:

logger = Dry.Logger(:my_app)
  .add_backend(
    stream: "logs/errors.log",
    log_if: :error?  # Only log ERROR and FATAL
  )

logger.info("Normal operation")  # Not logged to errors.log
logger.error("Something broke")  # Logged to errors.log

Available severity methods:

  • :debug? - Only DEBUG messages
  • :info? - Only INFO messages
  • :warn? - Only WARN messages
  • :error? - Only ERROR messages
  • :fatal? - Only FATAL messages

Using custom procs

For more complex filtering, use a proc that receives the log entry:

logger = Dry.Logger(:my_app)
  .add_backend(
    stream: "logs/requests.log",
    log_if: -> (entry) { entry.key?(:request) }
  )

logger.info("User logged in", request: true, path: "/login")
# Logged to logs/requests.log

logger.info("Cache cleared")
# Not logged to logs/requests.log

The entry object provides several useful methods:

log_if: -> (entry) {
  # Check severity
  entry.error? || entry.fatal?

  # Check for specific keys in payload
  entry.key?(:database)

  # Access payload values
  entry[:user_id] == 123

  # Check tags
  entry.tag?(:production)
}

Multiple conditions example

Route different types of logs to different files:

logger = Dry.Logger(:my_app) do |setup|
  # All logs in detailed format
  setup.add_backend(
    stream: "logs/all.log",
    template: :details
  )

  # Only errors in JSON format for monitoring tools
  setup.add_backend(
    stream: "logs/errors.json",
    formatter: :json,
    log_if: -> (entry) { entry.error? || entry.fatal? }
  )

  # Only HTTP requests
  setup.add_backend(
    stream: "logs/requests.log",
    formatter: :rack,
    log_if: -> (entry) { entry.key?(:verb) && entry.key?(:path) }
  )

  # Performance logs
  setup.add_backend(
    stream: "logs/performance.log",
    log_if: -> (entry) { entry.key?(:elapsed) }
  )
end

Backend configuration options

Each backend supports several configuration options:

logger.add_backend(
  stream: "logs/app.log",      # Output destination
  formatter: :json,             # Formatter to use (:string, :json, :rack)
  template: :details,           # Template for string formatter
  level: :warn,                 # Minimum level for this backend
  log_if: :error?,             # Conditional logging
  shift_age: 5,                # Log rotation: number of old files
  shift_size: 1048576,         # Log rotation: max file size
  colorize: true,              # Enable colorized output
  severity_colors: {           # Custom severity colors
    error: :red,
    warn: :yellow
  }
)

Using external loggers

Standard library logger

You can use Ruby's standard library Logger as a backend:

require "logger"

stdlib_logger = Logger.new("logs/stdlib.log")
stdlib_logger.formatter = proc { |severity, datetime, progname, msg|
  "[#{severity}] #{msg}\n"
}

logger = Dry.Logger(:my_app).add_backend(stdlib_logger)

logger.info("Test message")
# Written to both dry-logger default output and stdlib.log

Conditional external loggers

Apply conditional logging to external loggers too:

error_logger = Logger.new("logs/errors.log")

logger = Dry.Logger(:my_app)
  .add_backend(error_logger) { |backend|
    backend.log_if = :error?.to_proc
  }

Log rotation

dry-logger supports Ruby's Logger log rotation features for file-based backends.

Size-based rotation

Keep a fixed number of log files with a maximum size:

# Keep 5 log files, 10MB each
logger = Dry.Logger(:my_app,
  stream: "logs/app.log",
  shift_age: 5,
  shift_size: 10_485_760  # 10 megabytes
)

When app.log reaches 10MB, it's renamed to app.log.1, and a new app.log is created. This continues up to app.log.5, at which point the oldest file is deleted.

Time-based rotation

Rotate logs by time period:

# Rotate daily
logger = Dry.Logger(:my_app,
  stream: "logs/app.log",
  shift_age: "daily"
)

# Rotate weekly
logger = Dry.Logger(:my_app,
  stream: "logs/app.log",
  shift_age: "weekly"
)

# Rotate monthly
logger = Dry.Logger(:my_app,
  stream: "logs/app.log",
  shift_age: "monthly"
)

Rotated files are named with timestamps (e.g., app.log.20231015).

Custom rotation suffix

Customize the timestamp format for rotated files:

logger = Dry.Logger(:my_app,
  stream: "logs/app.log",
  shift_age: "monthly",
  shift_period_suffix: "month%m"  # e.g., app.log.month10
)

Managing backends

Closing backends

When you're done with a logger, close all backends to flush buffers and release file handles:

logger = Dry.Logger(:my_app, stream: "logs/app.log")

logger.info("Final message")

logger.close  # Flushes and closes all backends

Inspecting backends

View configured backends:

logger = Dry.Logger(:my_app)
  .add_backend(stream: "logs/app.log")

logger.backends
# => [#<Dry::Logger::Backends::Stream...>, #<Dry::Logger::Backends::File...>]

logger.backends.size
# => 2

octocatEdit on GitHub