Rails logging: request tags, Lograge, and JSON application events

Configure Rails logging levels and request tags, add explicit Lograge fields, preserve valid JSON output, and verify request and trace correlation in Guance.

Best practices
Illustration of logs flowing through a collection pipeline

Rails provides application logging, environment-specific levels, and request tags. Its detailed text output is convenient during development; in production, a structured request summary can make filtering and aggregation easier. Lograge supplies that summary, while ordinary Rails.logger calls still need their own formatting policy.

This guide covers both streams: configure levels and output, preserve request context, add explicit Lograge fields, and verify the collected records in Guance.

Understand the logger your application actually uses

Within a Rails application, use Rails.logger directly or the controller's logger helper:

Rails.logger.info("Order created order_id=#{order.id}")
Rails.logger.error("Payment callback signature verification failed")

The first example assumes an existing order object. Avoid interpolating full objects or sensitive data into messages.

Rails' framework default wraps ActiveSupport::Logger with ActiveSupport::TaggedLogging and uses a file in log/. Generated application configuration can override that. For example, the Rails 8.0 production template configures tagged output to stdout and reads RAILS_LOG_LEVEL, defaulting to info. Older applications may retain file output or a RAILS_LOG_TO_STDOUT conditional. Inspect config/environments/production.rb; upgrading Rails does not rewrite all existing configuration.

Default request output can include request start, controller action, queries, rendering, and completion messages. The exact output depends on level and framework configuration. It is useful diagnostic detail, but several lines per request require extra work to parse and correlate centrally.

Set the level and destination

A basic text logger with request-tag support can be configured as follows:

# config/environments/production.rb, inside Rails.application.configure
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
base_logger = ActiveSupport::Logger.new($stdout)
base_logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(base_logger)
config.log_tags = [:request_id]

Wrapping a replacement logger matters: assigning a plain ActiveSupport::Logger alone does not provide tagged logging. Stdout is convenient for containers when the runtime has an appropriate collection and retention policy. On hosts that write files, configure rotation, permissions, disk limits, and any required reopen behavior.

You can change Rails.logger.level = :debug within a process, but a separately started Rails console is not the running web worker. Changing its logger does not change other processes. A production-wide runtime switch needs an intentional control mechanism; changing RAILS_LOG_LEVEL normally takes effect after restarting the affected processes.

Attach request context

config.log_tags accepts request methods or callable values:

config.log_tags = [:request_id, :remote_ip]

With tagged text output, request-scoped events can receive prefixes such as [req_98765] [192.0.2.8]. Treat addresses according to your data policy and configure trusted proxies correctly before interpreting remote_ip.

Add a local service tag where useful:

Rails.logger.tagged("OrderService") do
  Rails.logger.info("Starting order validation")
end

Tags apply within the relevant logging context. They do not automatically propagate into a new background job, another process, or every custom logger. Text tags also do not automatically become JSON fields. Verify the actual output before expecting a collector to filter by request_id.

Make request summaries explicit with Lograge

Add the gem and install the bundle:

# Gemfile
gem "lograge"

Lograge changes Rails request logging into a completion summary. It does not convert every business log, third-party log, or exception message into one uniform event.

Start with explicit fields. This controller hook adds a request ID and, if your application defines current_user, a user ID to the notification payload:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protected

  def append_info_to_payload(payload)
    super
    payload[:request_id] = request.request_id
    payload[:current_user_id] = current_user&.id if respond_to?(:current_user, true)
  end
end

Use ActionController::API instead if that is your application's existing base class. Keep the project's authentication implementation; Rails does not create a current_user method merely because Lograge is installed.

Configure a dedicated summary logger whose formatter writes the JSON string unchanged. This prevents text tags from appearing before the JSON object:

# config/environments/production.rb, inside Rails.application.configure
require "time"

config.lograge.enabled = true
config.lograge.formatter = Lograge::Formatters::Json.new

request_logger = ActiveSupport::Logger.new($stdout)
request_logger.level = Logger::INFO
request_logger.formatter = proc do |_severity, _time, _progname, message|
  "#{message}\n"
end
config.lograge.logger = request_logger

config.lograge.custom_options = lambda do |event|
  {
    timestamp: Time.now.utc.iso8601(3),
    request_id: event.payload[:request_id],
    user_id: event.payload[:current_user_id]
  }.compact
end

The timestamp here is the time the summary is formatted, near request completion. It is not the request start time. The custom hook supplies the user ID; looking up an unpopulated payload key would otherwise yield no useful user value.

An illustrative summary has this shape; timings depend on the request and available instrumentation:

{"method":"GET","path":"/api/orders","format":"json","controller":"Api::OrdersController","action":"index","status":200,"duration":45.2,"view":12.3,"db":8.9,"timestamp":"2026-08-25T06:42:10.123Z","request_id":"req_98765","user_id":42}

Lograge request durations are expressed in milliseconds. View and database fields depend on what the framework reports. Inspect successful, failed, and API-only requests rather than assuming every record has every timing field.

Give business events a JSON formatter too

The preceding tagged logger is useful for text, but JSON summaries mixed with tagged business text still require two parsing paths. If the production stream should be entirely JSON, replace that application logger with a formatter that encodes tags as a field:

# config/environments/production.rb, inside Rails.application.configure
require "json"
require "time"

base_logger = ActiveSupport::Logger.new($stdout)
base_logger.formatter = ActiveSupport::Logger::SimpleFormatter.new
config.logger = ActiveSupport::TaggedLogging.new(base_logger)

# Replace the tagged formatter's call method so tags stay inside JSON.
config.logger.formatter.define_singleton_method(:call) do |severity, time, progname, message|
  event = {
    timestamp: time.utc.iso8601(3),
    level: severity,
    logger: progname,
    tags: current_tags,
    message: message.is_a?(Hash) ? nil : message.to_s,
    payload: message.is_a?(Hash) ? message : nil
  }.compact
  JSON.generate(event) + "\n"
end
config.log_tags = [:request_id]
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")

ActiveSupport::TaggedLogging supplies current_tags on the formatter. This example keeps tags as an array. With [:request_id], the first request tag can be normalized to request_id downstream; if you add more tags, document their meaning or adopt a formatter with named context fields. Do not assume the array is a trace context.

Business code can now pass safe structured values:

Rails.logger.info({ event: "order_created", order_id: order.id })

This small formatter is a starting schema, not a complete exception serializer. Add explicit exception class, message, and backtrace fields if your application needs them, and test the resulting JSON. Alternatively, use a logging library with a defined structured exception format, such as Semantic Logger.

Check the production configuration as a whole

Keep Lograge's request logger and the application logger intentional. Both can write one JSON object per line to stdout, but their schemas differ. The Lograge stream carries request_id directly; the formatter above carries tagged context separately. A formatter applied twice can quote an entire JSON string or add prefixes, so parse real output with a JSON parser before collecting it.

For container stdout, configure rotation and retention in the runtime or logging infrastructure. For application files, configure file management on the host. Writing to stdout does not itself impose a disk limit or retention period.

Environment-controlled levels make deployment behavior predictable. Temporary verbosity changes should be scoped and reverted. Request summaries are especially useful for slow-request queries and HTTP error counts, while business events explain what happened inside the operation.

Verify collection and correlation in Guance

Follow the DataKit log collector documentation for the actual file or container source. Send one known request and confirm its summary and business events arrive, with a consistent request identifier after parsing.

Lograge's status is an HTTP response code. Preserve it as http_status_code or another HTTP-specific field before mapping Guance's log-severity status. Keep duration, db, and view numeric and interpret them as milliseconds. Confirm timestamp precision and time zone.

For application performance monitoring (APM) correlation, inject the active instrumentation's real trace ID and ensure the corresponding trace is collected. An ordinary request ID cannot substitute for it. Only after those checks should you build slow-request views, 5xx detections, and alerts.

Frequently asked questions

What changed in Rails 7 and 8 production logging?

Generated templates and existing application files differ by version. The Rails 8.0 template explicitly uses tagged stdout logging and ENV.fetch("RAILS_LOG_LEVEL", "info"). Do not assume every Rails 7 or 8 application responds to RAILS_LOG_TO_STDOUT, or that upgrading the gem changes an older production file. Check the configuration actually deployed.

Should Lograge run in development too?

It can, especially when testing the production schema. For ordinary development, detailed request, SQL, and rendering output may be more convenient. Enable it per environment and keep a way to inspect the extra detail needed for an investigation.

How should Sidekiq logging fit in?

Sidekiq has its own logger and job context. Its configuration API accepts a logger through Sidekiq.configure_server { |config| config.logger = ... }, but adopting another logger can change formatting and context behavior. Keep a job ID in job events, propagate approved request/trace context explicitly at enqueue time, and test retries and errors. Sharing an output destination does not by itself unify the schemas.

Does parameter filtering protect custom events?

Rails config.filter_parameters helps redact matching parameters from framework logging. It is not a general-purpose scrubber for arbitrary strings, custom Lograge options, or business payloads. Use safe field allowlists and verify request and exception samples so that custom output does not bypass the filtering policy.

References and related reading

See Rails logging configuration, the Rails 8.0 production template, Lograge's configuration examples, and Sidekiq logging guidance.

Related guides cover logging best practices. Also see Ruby logging. The guides to Ruby logging libraries and microservices logging are currently in Chinese.

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting