Class: ActiveSupport::EventReporter

Inherits:
Object
  • Object
show all
Extended by:
Autoload
Defined in:
activesupport/lib/active_support/event_reporter.rb,
activesupport/lib/active_support/event_reporter/log_subscriber.rb

Overview

Active Support Event Reporter

ActiveSupport::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" }
#  }

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 serialize
    {
      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.

Subscribers are responsible for serializing event objects.

Subscribers

Subscribers must implement the emit method, which will be called with the event hash.

The event hash has 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)
context: Hash (The context 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)

Subscribers are responsible for encoding events to their desired format before emitting them to their target destination, such as a streaming platform, a log device, or an alerting service.

class JSONEventSubscriber
  def emit(event)
    json_data = JSON.generate(event)
    LogExporter.export(json_data)
  end
end

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

Note that event objects are passed through to subscribers as-is, and may need to be serialized before being encoded:

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

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

class LogSubscriber
  def emit(event)
    payload = event[:payload]
    json_data = JSON.generate(payload.serialize)
    LogExporter.export(json_data)
  end
end

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) }

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 },
#    context: {},
#    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 },
#    tags: {},
#    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 },
#  }

Security

When reporting events, Hash-based payloads are automatically filtered to remove sensitive data based on Rails.application.filter_parameters.

If an event object is given instead, subscribers will need to filter sensitive data themselves, e.g. with ActiveSupport::ParameterFilter.

Defined Under Namespace

Modules: TestHelper Classes: LogSubscriber

Class Attribute Summary collapse

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Autoload

autoload, autoload_at, autoload_under, eager_autoload, eager_load!

Constructor Details

#initialize(*subscribers, raise_on_error: false) ⇒ EventReporter

Returns a new instance of EventReporter.



293
294
295
296
297
298
# File 'activesupport/lib/active_support/event_reporter.rb', line 293

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

Class Attribute Details

.context_storeObject

:nodoc:



288
289
290
# File 'activesupport/lib/active_support/event_reporter.rb', line 288

def context_store
  @context_store
end

.filter_parametersObject

Filter parameters used to filter event payloads. If nil, Active Support’s filter parameters will be used instead.



287
288
289
# File 'activesupport/lib/active_support/event_reporter.rb', line 287

def filter_parameters
  @filter_parameters
end

Instance Attribute Details

#debug_mode=(value) ⇒ Object (writeonly)

:nodoc:



280
281
282
# File 'activesupport/lib/active_support/event_reporter.rb', line 280

def debug_mode=(value)
  @debug_mode = value
end

#raise_on_error=(value) ⇒ Object (writeonly)

Sets whether to raise an error if a subscriber raises an error during event emission, or when unexpected arguments are passed to notify.



278
279
280
# File 'activesupport/lib/active_support/event_reporter.rb', line 278

def raise_on_error=(value)
  @raise_on_error = value
end

#subscribersObject (readonly)

:nodoc



282
283
284
# File 'activesupport/lib/active_support/event_reporter.rb', line 282

def subscribers
  @subscribers
end

Instance Method Details

#clear_contextObject

Clears all context data.



532
533
534
# File 'activesupport/lib/active_support/event_reporter.rb', line 532

def clear_context
  context_store.clear
end

#contextObject

Returns the current context data.



537
538
539
# File 'activesupport/lib/active_support/event_reporter.rb', line 537

def context
  context_store.context
end

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

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.



442
443
444
445
446
447
448
449
450
# File 'activesupport/lib/active_support/event_reporter.rb', line 442

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

#debug_mode?Boolean

Check if debug mode is currently enabled. Debug mode is enabled on the reporter via with_debug, and in local environments.

Returns:

  • (Boolean)


427
428
429
# File 'activesupport/lib/active_support/event_reporter.rb', line 427

def debug_mode?
  @debug_mode || Fiber[:event_reporter_debug_mode]
end

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

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: {},
#    context: {},
#    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: {},
#    context: {},
#    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.



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'activesupport/lib/active_support/event_reporter.rb', line 370

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

  nil
end

#reload_payload_filterObject

:nodoc:



541
542
543
544
# File 'activesupport/lib/active_support/event_reporter.rb', line 541

def reload_payload_filter # :nodoc:
  @payload_filter = nil
  payload_filter
end

#set_context(context) ⇒ Object

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
#    source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
#  }


527
528
529
# File 'activesupport/lib/active_support/event_reporter.rb', line 527

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

#subscribe(subscriber, &filter) ⇒ Object

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)
context: Hash (The context 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) }


318
319
320
321
322
323
# File 'activesupport/lib/active_support/event_reporter.rb', line 318

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) ⇒ Object

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 },
#    context: {},
#    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 },
#    context: {},
#    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> },
#    context: {},
#    timestamp: 1738964843208679035,
#    source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
#  }


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

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

#unsubscribe(subscriber) ⇒ Object

Unregister an event subscriber. Accepts either a subscriber or a class.

subscriber = MyEventSubscriber.new
Rails.event.subscribe(subscriber)

Rails.event.unsubscribe(subscriber)
# or
Rails.event.unsubscribe(MyEventSubscriber)


333
334
335
# File 'activesupport/lib/active_support/event_reporter.rb', line 333

def unsubscribe(subscriber)
  @subscribers.delete_if { |s| subscriber === s[:subscriber] }
end

#with_debugObject

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


417
418
419
420
421
422
423
# File 'activesupport/lib/active_support/event_reporter.rb', line 417

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