Rails logger levels decide which messages reach your log file and which ones Rails quietly throws away. Set them too low and production drowns in noise you pay to store. Set them too high, or inconsistently, and the one line you needed at 2 a.m. was never written.

This guide covers what each level means, how config.log_level works across environments and Rails versions, and the mistakes that break most Rails logging setups.

See what each log level costs you

Middleware shows log volume by severity across every Rails service, so you know what debug chatter is actually costing.

TL;DR

  • Rails has six log levels: debug, info, warn, error, fatal, and unknown, numbered 0 to 5.
  • Rails writes a message only if its severity is equal to or higher than config.log_level.
  • The framework default is debug. Current Rails versions set production to info in the generated production.rb.
  • String interpolation in a log call runs even when that level is off. Use the block form for anything expensive.
  • Changing Rails.logger.level in a console affects only that console, not your running servers.
  • The most common failure is not picking the wrong level. It is picking levels inconsistently across a codebase.

What are Rails logger levels?

Rails logger levels are severity labels attached to every message written through Rails.logger. Each environment has a threshold, set by config.log_level. Anything below that threshold is discarded before it reaches disk or stdout.

The levels come from Ruby’s standard Logger class, which ActiveSupport::Logger wraps. It follows the same ranked model as log levels in other languages: you choose the cutoff, and everything below it is dropped.

A quick example with config.log_level = :warn:

Rails.logger.debug "cache miss for key=user:42"   # dropped
Rails.logger.info  "Order #1182 placed"          # dropped
Rails.logger.warn  "Connection pool at 90%"      # written
Rails.logger.error "Payment charge failed"       # written

What do the six Rails log levels mean?

Each level answers a different question about your app. As severity rises, volume should fall, and the need to act should grow.

LevelNumberWhat it meansTypical exampleAlert on it?
debug0Internal detail for someone actively investigatingWhich pricing rule a discount engine pickedNo
info1Normal, expected behavior worth recordingA user signed in, an order was placedNo
warn2Something unexpected, but the app still worksRetry succeeded on the second attemptOnly on trends
error3An operation failed and a user was affectedA payment charge failedYes
fatal4Unrecoverable, the process is about to exitRequired env var missing at bootPage someone
unknown5Catch-all for messages with no mapped severityRarely emitted on purposeInvestigate

A simple test: imagine the message showed up a hundred times an hour. If the answer is “nothing, that’s normal,” it belongs at info or below. If someone should look soon, it’s warn. If someone should be paged now, it’s error or fatal.

debug

Use debug to explain how the code made a decision: which branch it took, which rule matched, what an intermediate value was. It is off in production by default and turned on briefly during an investigation.

Rails.logger.debug { "Selected shipping_method=#{method} ruleset=#{ruleset.name}" }

info

Info records normal state changes: requests handled, jobs started and finished, orders placed. In a healthy app, most production log lines should be info.

Rails.logger.info "User #{current_user.id} signed in"

warn

Warn is an early signal. Nothing failed yet, but something is drifting: retries, a filling connection pool, a slow third-party API, a deprecated code path still in use.

Rails.logger.warn "Stripe call retried attempt=#{attempt} order=#{order.id}"

error

Error means an operation failed and a user felt it, even if the process keeps running. An occasional error is normal. A spike is what alerts should catch.

Rails.logger.error "Card charge failed order=#{order.id} error=#{e.class}"

fatal

Fatal is for conditions the app cannot survive, usually at boot. If the app keeps serving traffic after a fatal line, the level was wrong.

Rails.logger.fatal "SECRET_KEY_BASE missing. Refusing to boot."

unknown

Unknown exists for compatibility. Ruby’s Logger#add uses it when called with a nil severity. If unknown lines appear in your logs, something is calling the logger in an unusual way, and it is worth tracking down.

Rails debug vs info: what’s the difference?

The difference between debug and info is audience, not severity. Debug explains how the code decided something. Info records what the system did.

debuginfo
Written forThe person investigating a bug right nowAnyone reading logs later
Answers“Why did the code take this branch?”“What happened?”
On in production by defaultNoYes
ExampleSelected shipping_method=ground ruleset=holiday_2026Order #1182 placed by user=5531

If you are unsure, ask whether the message makes sense to someone who has never read that code path. If yes, it’s info. If it only makes sense to the person who wrote it, it’s debug.

What is the default log level in Rails?

Rails’ framework default is debug in every environment. What changes production is the line in the generated config/environments/production.rb.

EnvironmentEffective default
developmentdebug
testdebug
production (current Rails)info, set in the generated production.rb
production (Rails 7.1+)ENV.fetch(“RAILS_LOG_LEVEL”, “info”)
production (apps first generated on Rails 5)Often still debug

That last row matters. Upgrading Rails does not rewrite your existing environment files. If your app started on Rails 5, check production.rb before assuming you run at info. Deleting the config.log_level line entirely also drops production back to debug.

Where does Rails write logs?

In development and test, Rails writes to log/development.log and log/test.log. Recent Rails versions, including Rails 8.1, write production logs to STDOUT, which suits containers and log agents.

Since Rails 7.1, Rails.logger is an ActiveSupport::BroadcastLogger. It can send each line to several destinations at once:

Rails.logger.broadcast_to(ActiveSupport::Logger.new("log/audit.log"))

Setting level on the broadcast logger changes the level for every destination in it.

How do you change the log level in Rails?

Set config.log_level in the environment file you want to change. It accepts a symbol or a string.

Set the level per environment

# config/environments/development.rb
config.log_level = :debug

# config/environments/staging.rb
config.log_level = :info

# config/environments/production.rb
config.log_level = :info

Some teams run production at warn to cut volume. That works, but you lose the info-level context that explains what led up to an error. A common compromise is to keep info in production and filter it in your log pipeline, where you control retention.

Read the level from an environment variable

# config/environments/production.rb
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")

This lets you raise verbosity during an incident by changing one variable and restarting, with no code change or redeploy. Rails 7.1+ generates this line for you.

Can you change the log level without restarting?

Only inside the process where you run the change. Rails.logger.level = :debug In a Rails console, you change that console’s logger. Your Puma workers and Sidekiq processes keep their own loggers and their own levels.

To change the level for running servers, restart with a new RAILS_LOG_LEVEL, or build an admin endpoint or feature flag that sets Rails.logger.level inside each worker. To quiet one noisy block temporarily, use silence:

Rails.logger.silence(Logger::ERROR) do
  # only error and above are written inside this block
  legacy_importer.run
end

How do you check the current log level?

Rails.logger.level    # => 1 (info)
Rails.logger.debug?   # => false
Rails.logger.info?    # => true

Why is config.log_level being ignored?

The most common cause is setting a level on a custom logger. Rails applies config.log_level at boot and overrides it:

# The level here is overwritten at boot
config.logger = ActiveSupport::Logger.new(STDOUT, level: :warn)

# Set it here instead
config.log_level = :warn

Other causes to check:

  • A custom BroadcastLogger on an early Rails 7.1 release, where a bug made config.log_level have no effect. Upgrade to the latest 7.1 patch or later.
  • An initializer that sets Rails.logger.level after boot.
  • Checking the wrong process. A console and a Puma worker do not share a logger.
  • Sidekiq, which uses its own logger. See the Sidekiq section below.

How do you write log messages at each level?

Call the level method on Rails.logger from any controller, model, job, or mailer. This example applies the rules above:

class OrdersController < ApplicationController
  def create
    @order = Order.new(order_params)
    Rails.logger.debug { "New order attributes: #{@order.attributes.inspect}" }

    if @order.save
      Rails.logger.info "Order ##{@order.id} placed by user=#{current_user.id}"
      redirect_to @order
    else
      # Expected user input problem, not a system failure
      Rails.logger.info "Order validation failed: #{@order.errors.full_messages.join(', ')}"
      render :new, status: :unprocessable_entity
    end
  end
end

Validation failures are logged at info. Bad user input is normal traffic, not a system problem.

Log exceptions without losing the backtrace

Passing an exception object directly, as in Rails.logger.error(e), depends on the formatter. Ruby’s Logger::Formatter prints the backtrace. Rails’ SimpleFormatter prints only e.inspect, and the backtrace is gone. Be explicit instead:

begin
  PaymentGateway.charge!(order)
rescue PaymentGateway::Error => e
  Rails.logger.error(
    "Payment charge failed order=#{order.id} #{e.class}: #{e.message}n" 
    "#{Array(e.backtrace).first(15).join("n")}"
  )
  Rails.error.report(e, context: { order_id: order.id })  # Rails 7.0+
  raise
end

Rails.error.report sends the exception to any error subscribers you have configured, so your log line stays readable, and the full exception still reaches your error tracker.

What are the most common Rails logging mistakes?

The same few mistakes show up in most Rails codebases, and each has a simple fix.

Logging real failures at info because the app recovered

A payment retry that succeeds on the third attempt still had two failures. Log them at warn so a rising retry rate is visible before it becomes an outage.

Logging expected user errors at error

Failed validations, 404s on stale links, and wrong passwords are normal traffic. Logging them at error inflates your error rate and teaches the team to ignore error alerts.

Leaving debug on in production

At real traffic volumes, debug logging multiplies ingestion and storage costs and buries the lines you need during an incident. Active Record also logs every SQL query at debug. It often happens by accident, through an old Rails 5 production.rb or a deleted config.log_level line.

Interpolating expensive work into a disabled level

Ruby builds the string before Logger checks the level:

# expensive_report runs even when debug is off
Rails.logger.debug "Report: #{expensive_report}"

# expensive_report runs only when debug is on
Rails.logger.debug { "Report: #{expensive_report}" }

The same applies to .inspect on large Active Record objects and collections.

Choosing levels inconsistently

If one engineer logs cache misses at info and another at debug, no single threshold works for the whole app. Write your level rules down and enforce them in code review.

Stop paying to store debug logs

Middleware log pipelines filter and drop low-value lines by severity before they hit storage.

What are the best practices for Rails logging in production?

Correct levels are the foundation. These habits make the logs useful when you are actually debugging.

Tag every line with a request ID

# config/environments/production.rb
config.log_tags = [:request_id]

Every line from the same request now carries the same ID, so you can pull one request out of thousands of interleaved lines. For custom tags in jobs or services, use tagged logging:

Rails.logger.tagged("tenant=#{tenant.id}") do
  Rails.logger.info "Starting nightly sync"
end

Switch to structured JSON logs

Plain text is fine to read by eye. JSON fields are what let a log platform search, aggregate, and alert across millions of lines. Lograge collapses Rails’ multi-line request logs into one JSON line per request:

# Gemfile
gem "lograge"

# config/environments/production.rb
config.lograge.enabled = true
config.lograge.formatter = Lograge::Formatters::Json.new
config.lograge.custom_payload do |controller|
  { request_id: controller.request.request_id, user_id: controller.try(:current_user)&.id }
end

Lograge handles request logs only. Your own Rails.logger.info calls still use the logger’s formatter, so swap in a JSON formatter for those:

class JsonLogFormatter < ::Logger::Formatter
  def call(severity, time, _progname, msg)
    {
      timestamp: time.utc.iso8601(3),
      level: severity.downcase,
      message: msg.is_a?(String) ? msg : msg.inspect
    }.to_json + "n"
  end
end

# config/environments/production.rb
config.log_formatter = JsonLogFormatter.new

Lograge or Semantic Logger? Lograge only reformats request logs. Semantic Logger replaces the Rails logger entirely and adds structured output, per-class log levels, and asynchronous writes.

Start with Lograge. Move to Semantic Logger when you need per-class levels or high-volume async logging. For field naming conventions that hold up across services, see our guide to structured logging best practices.

Set log levels for Sidekiq and Active Job

Sidekiq keeps its own logger, so config.log_level does not control it:

Sidekiq.configure_server { |config| config.logger.level = Logger::WARN }

To quiet Active Job’s per-job lines without lowering your app’s level, give it its own logger:

config.active_job.logger = ActiveSupport::Logger.new(STDOUT, level: :warn)

Never log secrets

Passwords, API keys, card numbers, and personal data should never reach a log line at any level. Rails filters request parameters listed in config/initializers/filter_parameter_logging.rb:

Rails.application.config.filter_parameters += [
  :passw, :secret, :token, :_key, :crypt, :otp, :ssn, :card_number
]

This protects request logs only. Anything you interpolate yourself is still your responsibility.

Keep verbose query logs out of production

config.active_record.verbose_query_logs shows the line of code behind each SQL query. It is useful in development, but it walks the call stack for every query. Rails enables it only in development.rb. Leave it there.

How do log levels work once Rails logs are centralized?

Once logs leave individual servers, levels become the input for alerts, pipelines, and cost control. Tailing production.log works for one monolith. It breaks down with multiple services, Sidekiq workers, and Kubernetes pods.

In a centralized setup like log monitoring in Middleware, levels map to actions:

  • error and fatal trigger alerts.
  • warn trends feed anomaly detection before something breaks.
  • info gives the surrounding context once an incident is open.
  • debug is filtered or dropped at ingestion, so you don’t pay to store it.

This only works if levels mean the same thing everywhere. A Rails warn and Python’s logging levels WARNING should describe the same kind of event, or cross-service queries by severity mislead you.

How do Rails log levels map to OpenTelemetry severity?

When Rails logs pass through an OpenTelemetry pipeline, each level is converted to a standard SeverityNumber. That is what lets a Rails warn and a Java WARNING be queried as the same thing.

Rails levelSeverityTextSeverityNumber (base)
debugDEBUG5
infoINFO9
warnWARN13
errorERROR17
fatalFATAL21
unknownDepends on the bridgeOften 0 (unspecified)

Rails has no trace level, so debug covers both. If you alert on severity across services, alert on SeverityNumber >= 17 rather than on level names.

Add trace IDs to Rails logs

A request ID connects lines inside one Rails process. A trace ID connects a log line to the distributed trace for the whole request, across every service it touched. If you use OpenTelemetry in Rails, add the current trace ID to your Lograge payload:

config.lograge.custom_payload do |controller|
  span_context = OpenTelemetry::Trace.current_span.context
  {
    request_id: controller.request.request_id,
    trace_id: span_context.valid? ? span_context.hex_trace_id : nil
  }
end

With a trace ID in every request log, you can jump from an error line to the trace that produced it. The OpenTelemetry logs guide covers this model in depth, and the same field powers correlating logs with traces and metrics on a single timeline.

Send Rails logs and traces to Middleware

Middleware’s Ruby APM is built on OpenTelemetry and covers traces and profiling for Rails, Sinatra, and Rack apps. See the Middleware Ruby APM setup guide. Logs are collected by the Middleware Infra Agent, which reads JSON-structured logs from your hosts and containers. You can then filter them by level, service, and custom attributes in Middleware’s log explorer.

Structured JSON output with consistent level and trace_id fields is what makes both halves line up.

Put your Rails logs next to your traces

Centralize Rails logs, APM traces, and infrastructure metrics in one place. Free Forever plan included.

FAQs

What are the log levels in Rails?

Rails has six log levels: debug, info, warn, error, fatal, and unknown, numbered 0 to 5. They come from Ruby’s standard Logger class, which ActiveSupport::Logger wraps.

What is the default Rails log level?

The framework default is debug. Current Rails versions set production to info in the generated production.rb, and Rails 7.1+ reads it from the RAILS_LOG_LEVEL environment variable with info as the fallback. Apps first generated on Rails 5 may still have debug in production.

How do I change the log level in Rails?

Set config.log_level in the relevant file in config/environments/, for example config.log_level = :warn. To change it without editing code, use config.log_level = ENV.fetch(“RAILS_LOG_LEVEL”, “info”) and restart with a new value.

Can I change the Rails log level without restarting?

Only per process. Rails.logger.level = :debug affects the process it runs in, so running it in a console does not touch your web or job workers. For running servers, restart with a new RAILS_LOG_LEVEL or build a runtime switch that each worker applies.

What’s the difference between debug and info in Rails?

Debug explains how the code made a decision, for someone actively troubleshooting. Info records what the application did, in a way anyone can understand later without knowing the implementation.

Where are Rails logs stored?

In log/<environment>.log for development and test. Recent Rails versions write production logs to STDOUT so a container runtime or log agent can collect them.

Why aren’t my debug logs showing up?

Your config.log_level is above debug, usually info in production. Check the environment file and the RAILS_LOG_LEVEL variable, then run Rails.logger.level in the same process type to confirm the active value.

Why is Rails logging SQL queries in production?

Your production level is debug, because Active Record logs SQL at debug. Check config.log_level in production.rb and the RAILS_LOG_LEVEL variable.

Can I set a different log level for one class or gem?

Not with the built-in logger, which has one level per logger. Give that component its own logger instance, or use Semantic Logger, which supports per-class levels.

Does logging slow down a Rails app?

Yes, a little. Every written line costs I/O, so debug-level logging costs more than error-level logging. The bigger risk is expensive string interpolation, which runs even when the level is disabled. Use Rails.logger.debug { … } to defer the work.

Should each environment use a different log level?

Yes. Development and test benefit from debug while you are actively working. Staging and production usually run at info or higher, which keeps logs focused on operational signal and keeps ingestion costs predictable.