123456789_123456789_123456789_123456789_123456789_

Class: ActiveSupport::EventReporter

Relationships & Source Files
Namespace Children
Modules:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, Autoload
Inherits: Object
Defined in: activesupport/lib/active_support/event_reporter.rb,
activesupport/lib/active_support/event_reporter/json_encoder.rb,
activesupport/lib/active_support/event_reporter/message_pack_encoder.rb

Overview

EventReporter provides an interface for reporting structured events to subscribers.

To report an event, you can use the #notify method:

Rails.event.notify("user_created", { id: 123 })
# Emits event:
#  {
#    name: "user_created",
#    payload: { id: 123 },
#    timestamp: 1738964843208679035,
#    source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
#  }

Filtered Subscriptions

Subscribers can be configured with an optional filter proc to only receive a subset of events:

# Only receive events with names starting with "user."
Rails.event.subscribe(user_subscriber) { |event| event[:name].start_with?("user.") }

# Only receive events with specific payload types
Rails.event.subscribe(audit_subscriber) { |event| event[:payload].is_a?(AuditEvent) }

The #notify API can receive either an event name and a payload hash, or an event object. Names are coerced to strings.

Event Objects

If an event object is passed to the #notify API, it will be passed through to subscribers as-is, and the name of the object’s class will be used as the event name.

class UserCreatedEvent

def initialize(id:, name:)
  @id = id
  @name = name
end

def to_h
  {
    id: @id,
    name: @name
  }
end

end

Rails.event.notify(UserCreatedEvent.new(id: 123, name: "John Doe"))
# Emits event:
#  {
#    name: "UserCreatedEvent",
#    payload: #<UserCreatedEvent:0x111>,
#    timestamp: 1738964843208679035,
#    source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
#  }

An event is any Ruby object representing a schematized event. While payload hashes allow arbitrary, implicitly-structured data, event objects are intended to enforce a particular schema.

Default Encoders

::Rails provides default encoders for common serialization formats. Event objects and tags MUST implement to_h to be serialized.

class JSONLogSubscriber
  def emit(event)
    # event = { name: "UserCreatedEvent", payload: { UserCreatedEvent: #<UserCreatedEvent:0x111> } }
    json_data = ActiveSupport::EventReporter::JSONEncoder.encode(event)
    # => {
    #      "name": "UserCreatedEvent",
    #      "payload": {
    #        "id": 123,
    #        "name": "John Doe"
    #      }
    #    }
    Rails.logger.info(json_data)
  end
end

class MessagePackSubscriber
  def emit(event)
    msgpack_data = ActiveSupport::EventReporter::MessagePackEncoder.encode(event)
    BatchExporter.export(msgpack_data)
  end
end

Debug Events

You can use the #debug method to report an event that will only be reported if the event reporter is in debug mode:

Rails.event.debug("my_debug_event", { foo: "bar" })

Tags

To add additional context to an event, separate from the event payload, you can add tags via the #tagged method:

Rails.event.tagged("graphql") do
  Rails.event.notify("user_created", { id: 123 })
end

# Emits event:
#  {
#    name: "user_created",
#    payload: { id: 123 },
#    tags: { graphql: true },
#    timestamp: 1738964843208679035,
#    source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
#  }

Context Store

You may want to attach metadata to every event emitted by the reporter. While tags provide domain-specific context for a series of events, context is scoped to the job / request and should be used for metadata associated with the execution context. Context can be set via the #set_context method:

Rails.event.set_context(request_id: "abcd123", user_agent: "TestAgent")
Rails.event.notify("user_created", { id: 123 })

# Emits event:
#  {
#    name: "user_created",
#    payload: { id: 123 },
#    context: { request_id: "abcd123", user_agent: TestAgent" },
#    timestamp: 1738964843208679035,
#    source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
#  }

Context is reset automatically before and after each request.

A custom context store can be configured via config.active_support.event_reporter_context_store.

# config/application.rb
config.active_support.event_reporter_context_store = CustomContextStore

class CustomContextStore
  class << self
    def context
      # Return the context.
    end

    def set_context(context_hash)
      # Append context_hash to the existing context store.
    end

    def clear
      # Delete the stored context.
    end
  end
end

The Event Reporter standardizes on symbol keys for all payload data, tags, and context store entries. ::String keys are automatically converted to symbols for consistency.

Rails.event.notify("user.created", { "id" => 123 })
# Emits event:
#  {
#    name: "user.created",
#    payload: { id: 123 },
#  }

Class Attribute Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(*subscribers, raise_on_error: false, tags: nil) ⇒ EventReporter

[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 245

def initialize(*subscribers, raise_on_error: false, tags: nil)
  @subscribers = []
  subscribers.each { |subscriber| subscribe(subscriber) }
  @raise_on_error = raise_on_error
end

Class Attribute Details

.context_store (rw)

This method is for internal use only.
[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 240

attr_accessor :context_store # :nodoc:

Instance Attribute Details

#debug_mode?Boolean (readonly)

Check if debug mode is currently enabled.

[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 361

def debug_mode?
  Fiber[:event_reporter_debug_mode]
end

#raise_on_error=(value) (rw)

This method is for internal use only.
[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 236

attr_writer :raise_on_error # :nodoc:

#raise_on_error?Boolean (rw, private)

[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 472

def raise_on_error?
  @raise_on_error
end

#subscribers (readonly)

[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 237

attr_reader :subscribers

Instance Method Details

#clear_context

Clears all context data.

[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 462

def clear_context
  context_store.clear
end

#context

Returns the current context data.

[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 467

def context
  context_store.context
end

#context_store (private)

[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 476

def context_store
  self.class.context_store
end

#debug(name_or_object, payload = nil, caller_depth: 1, **kwargs)

Report an event only when in debug mode. For example:

Rails.event.debug("sql.query", { sql: "SELECT * FROM users" })

Arguments

  • :payload - The event payload when using string/symbol event names.

  • :caller_depth - The stack depth to use for source location (default: 1).

  • :kwargs - Additional payload data when using string/symbol event names.

[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 376

def debug(name_or_object, payload = nil, caller_depth: 1, **kwargs)
  if debug_mode?
    if block_given?
      notify(name_or_object, payload, caller_depth: caller_depth + 1, **kwargs.merge(yield))
    else
      notify(name_or_object, payload, caller_depth: caller_depth + 1, **kwargs)
    end
  end
end

#handle_unexpected_args(name_or_object, payload, kwargs) (private)

[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 504

def handle_unexpected_args(name_or_object, payload, kwargs)
  message = <<~MESSAGE
    Rails.event.notify accepts either an event object, a payload hash, or keyword arguments.
    Received: #{name_or_object.inspect}, #{payload.inspect}, #{kwargs.inspect}
  MESSAGE

  if raise_on_error?
    raise ArgumentError, message
  else
    ActiveSupport.error_reporter.report(ArgumentError.new(message), handled: true)
  end
end

#notify(name_or_object, payload = nil, caller_depth: 1, **kwargs)

Reports an event to all registered subscribers. An event name and payload can be provided:

Rails.event.notify("user.created", { id: 123 })
# Emits event:
#  {
#    name: "user.created",
#    payload: { id: 123 },
#    tags: {},
#    timestamp: 1738964843208679035,
#    source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
#  }

Alternatively, an event object can be provided:

Rails.event.notify(UserCreatedEvent.new(id: 123))
# Emits event:
#  {
#    name: "UserCreatedEvent",
#    payload: #<UserCreatedEvent:0x111>,
#    tags: {},
#    timestamp: 1738964843208679035,
#    source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
#  }

Arguments

  • :payload - The event payload when using string/symbol event names.

  • :caller_depth - The stack depth to use for source location (default: 1).

  • :kwargs - Additional payload data when using string/symbol event names.

[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 307

def notify(name_or_object, payload = nil, caller_depth: 1, **kwargs)
  name = resolve_name(name_or_object)
  payload = resolve_payload(name_or_object, payload, **kwargs)

  event = {
    name: name,
    payload: payload,
    tags: TagStack.tags,
    context: context_store.context,
    timestamp: Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond),
  }

  caller_location = caller_locations(caller_depth, 1)&.first

  if caller_location
    source_location = {
      filepath: caller_location.path,
      lineno: caller_location.lineno,
      label: caller_location.label,
    }
    event[:source_location] = source_location
  end

  subscribers.each do |subscriber_entry|
    subscriber = subscriber_entry[:subscriber]
    filter = subscriber_entry[:filter]

    next if filter && !filter.call(event)

    subscriber.emit(event)
  rescue => subscriber_error
    if raise_on_error?
      raise
    else
      ActiveSupport.error_reporter.report(subscriber_error, handled: true)
    end
  end
end

#resolve_name(name_or_object) (private)

[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 480

def resolve_name(name_or_object)
  case name_or_object
  when String, Symbol
    name_or_object.to_s
  else
    name_or_object.class.name
  end
end

#resolve_payload(name_or_object, payload, **kwargs) (private)

[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 489

def resolve_payload(name_or_object, payload, **kwargs)
  case name_or_object
  when String, Symbol
    handle_unexpected_args(name_or_object, payload, kwargs) if payload && kwargs.any?
    if kwargs.any?
      kwargs.transform_keys(&:to_sym)
    elsif payload
      payload.transform_keys(&:to_sym)
    end
  else
    handle_unexpected_args(name_or_object, payload, kwargs) if payload || kwargs.any?
    name_or_object
  end
end

#set_context(context)

Sets context data that will be included with all events emitted by the reporter. Context data should be scoped to the job or request, and is reset automatically before and after each request and job.

Rails.event.set_context(user_agent: "TestAgent")
Rails.event.set_context(job_id: "abc123")
Rails.event.tagged("graphql") do
  Rails.event.notify("user_created", { id: 123 })
end

# Emits event:
#  {
#    name: "user_created",
#    payload: { id: 123 },
#    tags: { graphql: true },
#    context: { user_agent: "TestAgent", job_id: "abc123" },
#    timestamp: 1738964843208679035
#  }
[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 457

def set_context(context)
  context_store.set_context(context)
end

#subscribe(subscriber, &filter)

Registers a new event subscriber. The subscriber must respond to

emit(event: Hash)

The event hash will have the following keys:

name: String (The name of the event)
payload: Hash, Object (The payload of the event, or the event object itself)
tags: Hash (The tags of the event)
timestamp: Float (The timestamp of the event, in nanoseconds)
source_location: Hash (The source location of the event, containing the filepath, lineno, and label)

An optional filter proc can be provided to only receive a subset of events:

Rails.event.subscribe(subscriber) { |event| event[:name].start_with?("user.") }
Rails.event.subscribe(subscriber) { |event| event[:payload].is_a?(UserEvent) }
[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 268

def subscribe(subscriber, &filter)
  unless subscriber.respond_to?(:emit)
    raise ArgumentError, "Event subscriber #{subscriber.class.name} must respond to #emit"
  end

  @subscribers << { subscriber: subscriber, filter: filter }
end

#tagged(*args, **kwargs, &block)

Add tags to events to supply additional context. Tags operate in a stack-oriented manner, so all events emitted within the block inherit the same set of tags. For example:

Rails.event.tagged("graphql") do
  Rails.event.notify("user.created", { id: 123 })
end

# Emits event:
# {
#    name: "user.created",
#    payload: { id: 123 },
#    tags: { graphql: true },
#    timestamp: 1738964843208679035,
#    source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
#  }

Tags can be provided as arguments or as keyword arguments, and can be nested:

Rails.event.tagged("graphql") do
# Other code here...
  Rails.event.tagged(section: "admin") do
    Rails.event.notify("user.created", { id: 123 })
  end
end

# Emits event:
#  {
#    name: "user.created",
#    payload: { id: 123 },
#    tags: { section: "admin", graphql: true },
#    timestamp: 1738964843208679035,
#    source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
#  }

The tagged API can also receive a tag object:

graphql_tag = GraphqlTag.new(operation_name: "user_created", operation_type: "mutation")
Rails.event.tagged(graphql_tag) do
  Rails.event.notify("user.created", { id: 123 })
end

# Emits event:
#  {
#    name: "user.created",
#    payload: { id: 123 },
#    tags: { "GraphqlTag": #<GraphqlTag:0x111> },
#    timestamp: 1738964843208679035,
#    source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
#  }
[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 435

def tagged(*args, **kwargs, &block)
  TagStack.with_tags(*args, **kwargs, &block)
end

#with_debug

Temporarily enables debug mode for the duration of the block. Calls to #debug will only be reported if debug mode is enabled.

Rails.event.with_debug do
  Rails.event.debug("sql.query", { sql: "SELECT * FROM users" })
end
[ GitHub ]

  
# File 'activesupport/lib/active_support/event_reporter.rb', line 352

def with_debug
  prior = Fiber[:event_reporter_debug_mode]
  Fiber[:event_reporter_debug_mode] = true
  yield
ensure
  Fiber[:event_reporter_debug_mode] = prior
end