ruby 94 lines · 3 tabs

Lograge-Style JSON Logging Without Extra Gems

Shared by codesnips Jan 2026
3 tabs
class JsonLogFormatter < ActiveSupport::Logger::SimpleFormatter
  def call(severity, timestamp, _progname, message)
    if message.is_a?(Hash)
      entry = {
        ts: timestamp.utc.iso8601(3),
        level: severity
      }.merge(current_tags).merge(message)
      "#{entry.to_json}\n"
    else
      "#{message}\n"
    end
  end

  def self.tag(**pairs)
    Thread.current[:log_tags] ||= {}
    Thread.current[:log_tags].merge!(pairs)
  end

  def self.reset_tags
    Thread.current[:log_tags] = {}
  end

  private

  def current_tags
    Thread.current[:log_tags] || {}
  end
end
3 files · ruby Explain with highlit

This snippet builds a lograge-style single-line JSON log for every request using only Rails' own building blocks, no external gems. The core idea is to replace Rails' noisy multi-line development log (which emits separate lines for Started, Processing, SQL, and Completed) with exactly one structured event per request that a log aggregator like Loki, Datadog, or CloudWatch can parse.

In JsonLogFormatter, a custom ActiveSupport::Logger::SimpleFormatter subclass is defined. When the message passed to the logger is already a Hash, it merges in a timestamp, severity, and any thread-local request tags, then serializes with to_json. Non-hash messages fall back to a plain string so unexpected logger.info "..." calls still work. Thread-local storage via Thread.current[:log_tags] is used so tags set anywhere in the request cycle end up on the final line without threading them through method arguments.

The real work happens in RequestLogSubscriber, which subscribes to the process_action.action_controller notification. Rails fires this event once per controller action with a rich payload and timing data, which makes it the natural single place to summarize a request. The subscriber flattens controller, action, status, method, and path, and derives duration_ms from event.duration. It also pulls db_runtime and view_runtime out of the payload — these are already computed by Rails, so no manual instrumentation is needed. Exceptions are captured from payload[:exception] so failed requests are logged with an error field and an appropriate status. The format_exception helper normalizes the [class, message] array Rails provides.

Wiring lives in initializers/structured_logging.rb. The formatter is attached to Rails.logger, the default ActionController::LogSubscriber is detached to silence the built-in per-action logs, and the custom subscriber is attached instead. This avoids double logging.

The trade-off is that this approach only summarizes controller actions; per-query SQL lines disappear, which is usually desirable in production but occasionally missed while debugging. It also depends on process_action payload keys that are stable but framework-internal. When a team wants clean JSON logs without adding a dependency, this pattern delivers most of lograge's value in about sixty lines.


Related snips

Share this code

Here's the card — post it anywhere.

Lograge-Style JSON Logging Without Extra Gems — share card
Link copied