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
- .context_store rw Internal use only
Class Method Summary
Autoload
- Extended
Instance Attribute Summary
-
#debug_mode? ⇒ Boolean
readonly
Check if debug mode is currently enabled.
- #subscribers readonly
- #raise_on_error? ⇒ Boolean rw private
- #raise_on_error=(value) rw Internal use only
Instance Method Summary
-
#clear_context
Clears all context data.
-
#context
Returns the current context data.
-
#debug(name_or_object, payload = nil, caller_depth: 1, **kwargs)
Report an event only when in debug mode.
-
#notify(name_or_object, payload = nil, caller_depth: 1, **kwargs)
Reports an event to all registered subscribers.
-
#set_context(context)
Sets context data that will be included with all events emitted by the reporter.
-
#subscribe(subscriber, &filter)
Registers a new event subscriber.
-
#tagged(*args, **kwargs, &block)
Add tags to events to supply additional context.
-
#with_debug
Temporarily enables debug mode for the duration of the block.
- #context_store private
- #handle_unexpected_args(name_or_object, payload, kwargs) private
- #resolve_name(name_or_object) private
- #resolve_payload(name_or_object, payload, **kwargs) private
Constructor Details
.new(*subscribers, raise_on_error: false, tags: nil) ⇒ EventReporter
# 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)
# 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.
# File 'activesupport/lib/active_support/event_reporter.rb', line 361
def debug_mode? Fiber[:event_reporter_debug_mode] end
#raise_on_error=(value) (rw)
# 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.
# File 'activesupport/lib/active_support/event_reporter.rb', line 462
def clear_context context_store.clear end
#context
Returns the current context data.
# 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.
# 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 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, else ActiveSupport.error_reporter.report(ArgumentError.new( ), 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.
# 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., 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 ]#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
# }
# 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)
: Hash (The of the event)
: Float (The 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) }
# 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" }
# }
# File 'activesupport/lib/active_support/event_reporter.rb', line 435
def tagged(*args, **kwargs, &block) TagStack. (*args, **kwargs, &block) end
#with_debug
# 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