Rails 8.1 Introduces Structured Event Reporting with Rails.event

Introduction

Modern observability platforms thrive on structured data. They can parse JSON, extract fields, build dashboards, and alert on specific conditions. But Rails has traditionally given us Rails.logger, which produces human readable but unstructured log lines.

Parsing these logs for analytics is painful. We end up writing regex patterns, hoping the log format doesn’t change, and losing valuable context along the way.

Rails 8.1 introduces a first class solution: the Structured Event Reporter, accessible via Rails.event.

Before

Before this change, logging in Rails meant working with unstructured text.

Rails.logger.info("User created: id=#{user.id}, name=#{user.name}")

This produces a log line like:

User created: id=123, name=John Doe

To extract meaningful data from this, observability tools need to parse the string. If we change the format slightly, our parsing breaks.

We also lack consistent metadata. Where did this log come from? What request triggered it? What was the timestamp with nanosecond precision?

Teams often build custom solutions, wrapping loggers with JSON formatters, manually adding request IDs, and hoping everyone follows the same conventions.

After

Rails 8.1 provides Rails.event, a structured event reporter that emits events with consistent metadata.

Rails.event.notify("user.signup", user_id: 123, email: "[email protected]")

This produces a structured event:

{
  name: "user.signup",
  payload: { user_id: 123, email: "[email protected]" },
  tags: {},
  context: { request_id: "abc123", user_agent: "Mozilla..." },
  timestamp: 1738964843208679035, # nanosecond precision

  source_location: {
    filepath: "app/services/user_service.rb",
    lineno: 45,
    label: "UserService#create"
  }
}

Every event includes standardized metadata: the event name, payload, tags, context, timestamp, and the exact source location where it was emitted.

Adding Tags and Context

The Event Reporter provides two mechanisms for enriching events: tags and context.

Tags

Tags add domain specific context that nests. All events within a tagged block inherit those tags.

Consider an e-commerce application where we want to track all events happening within the checkout flow:

class CheckoutsController < ApplicationController
  def create
    Rails.event.tagged("checkout") do
      Rails.event.notify("checkout.started", cart_id: @cart.id, item_count: @cart.items.count)

      process_payment
      create_order

      Rails.event.notify("checkout.completed", order_id: @order.id, total: @order.total)
    end
  end
end

All events within the block will include tags: { checkout: true }, making it easy to filter and analyze checkout related events in our observability platform.

Tags can also be nested for more granular categorization:

class Admin::OrdersController < ApplicationController
  def update
    Rails.event.tagged("api") do
      Rails.event.tagged(version: "v2", section: "admin") do
        Rails.event.notify("order.updated", order_id: @order.id, status: @order.status)
        # Event includes tags: { api: true, version: "v2", section: "admin" }

      end
    end
  end
end

This is useful when we want to categorize events by feature area, API version, or any other dimension.

Context

Context is designed for request or job scoped metadata that spans the entire execution.

A common use case is setting context in a middleware or controller callback so all events in the request include relevant metadata:

class ApplicationController < ActionController::Base
  before_action :set_event_context

  private

  def set_event_context
    Rails.event.set_context(
      request_id: request.request_id,
      user_id: current_user&.id,
      ip_address: request.remote_ip,
      user_agent: request.user_agent
    )
  end
end

Now every event emitted during the request will automatically include this context:

class OrdersController < ApplicationController
  def create
    @order = Order.create!(order_params)
    Rails.event.notify("order.created", order_id: @order.id, total: @order.total)
    # Event automatically includes context: { request_id: "...", user_id: 123, ... }

  end
end

Context expands over the course of a unit of work and attaches to every event automatically.

Debug Mode

Sometimes we want verbose logging during development but not in production. The Event Reporter supports conditional debug events.

Rails.event.with_debug do
  Rails.event.debug("sql.query", sql: "SELECT * FROM users WHERE id = ?", binds: [123])
  # Only reported when debug mode is active

end

This replaces the need for log levels. We either emit a regular event with notify, or a debug event with debug that only fires in debug mode.

Schematized Events

For teams that want strongly typed events, the Event Reporter accepts event objects.

class OrderCompletedEvent
  attr_reader :order_id, :total, :item_count, :payment_method

  def initialize(order_id:, total:, item_count:, payment_method:)
    @order_id = order_id
    @total = total
    @item_count = item_count
    @payment_method = payment_method
  end
end

Rails.event.notify(
  OrderCompletedEvent.new(
    order_id: 456,
    total: 99.99,
    item_count: 3,
    payment_method: "credit_card"
  )
)

This allows us to define schemas for our events, ensuring consistency across the codebase. The event object is passed directly to subscribers in the payload field, and subscribers can handle serialization based on the object type.

Subscribing to Events

The Event Reporter separates emission from processing. Applications register subscribers to control how events are serialized and emitted. Subscribers must implement an #emit method, which receives the event hash.

class LogSubscriber
  def emit(event)
    payload = event[:payload].map { |key, value| "#{key}=#{value}" }.join(" ")
    source_location = event[:source_location]
    log = "[#{event[:name]}] #{payload} at #{source_location[:filepath]}:#{source_location[:lineno]}"
    Rails.logger.info(log)
  end
end

Rails.event.subscribe(LogSubscriber.new)

This would produce a log line like:

[user.signup] user_id=123 [email protected] at app/services/user_service.rb:45

Multiple subscribers can be registered, allowing events to flow to different destinations: a logging pipeline, metrics system, or analytics platform.

class DatadogSubscriber
  def emit(event)
    Datadog.log(event.to_json)
  end
end

Rails.event.subscribe(LogSubscriber.new)
Rails.event.subscribe(DatadogSubscriber.new)

Testing Support

The Event Reporter includes test helpers for asserting on event emission.

# Assert specific events are reported

assert_event_reported("user.created", payload: { id: 123 }) do
  UserService.create_user(name: "John")
end

# Assert no events are reported

assert_no_event_reported("user.deleted") do
  UserService.update_user(id: 123, name: "Jane")
end

This makes it easy to verify that our code emits the right events with the expected payloads.

Key Design Decisions

Fiber based Isolation

Tags and context are scoped per fiber. Child fibers and threads inherit context from the parent, but the context is copied on write. Changes in a child do not propagate back to the parent.

No Log Levels

The API intentionally avoids traditional log levels like info, warn, error. These are often used inconsistently and interpreted differently across teams.

Instead, the API provides two methods:

  • notify for events users care about
  • debug for events developers care about during debugging

Payload vs Tags vs Context

These three serve different purposes:

  • Payload: Information about the specific event that occurred
  • Tags: Domain specific context that forms a stack within blocks
  • Context: Request or job level metadata that spans the entire execution

Conclusion

Structured event reporting brings Rails logging into the modern observability era. Instead of parsing unstructured text, we get consistent, machine readable events with rich metadata out of the box.

The Rails.event API provides a unified interface for structured logs, business events, and telemetry. Combined with the subscriber pattern, teams can route events to any destination they need.

This feature originated from Shopify’s production needs and is now available for all Rails 8.1 applications.

References

Need help on your Ruby on Rails or React project?

Join Our Newsletter