Skip to content

Commit 517cb61

Browse files
Add Structured Event Reporter
Ref: rails#50452 This adds a Structured Event Reporter to Rails, accessible via `Rails.event`. It allows you to report events to a subscriber, and provides mechanisms for adding tags and context to events. Events encompass "structured logs", but also "business events", as well as telemetry events such as metrics and logs. The Event Reporter is designed to be a single interface for producing any kind of event in a Rails application. We separate the emission of events from how these events reach end consumers; applications are expected to define their own subscribers, and the Event Reporter is responsible for emitting events to these subscribers.
1 parent 7533bcb commit 517cb61

File tree

17 files changed

+1455
-0
lines changed

17 files changed

+1455
-0
lines changed

actionpack/lib/action_dispatch.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class MissingController < NameError
7070
autoload :HostAuthorization
7171
autoload :RequestId
7272
autoload :Callbacks
73+
autoload :ClearEventReporterContext
7374
autoload :Cookies
7475
autoload :ActionableExceptions
7576
autoload :DebugExceptions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
module ActionDispatch
4+
# Middleware that sets up a callback on rack.response_finished to clear
5+
# the EventReporter context when the response is finished. This ensures that
6+
# context is cleared as late as possible in the request lifecycle.
7+
class ClearEventReporterContext # :nodoc:
8+
def initialize(app)
9+
@app = app
10+
end
11+
12+
def call(env)
13+
response = @app.call(env)
14+
15+
env["rack.response_finished"] ||= []
16+
env["rack.response_finished"] << -> do
17+
ActiveSupport.event_reporter.clear_context
18+
end
19+
20+
response
21+
rescue Exception => e
22+
ActiveSupport.event_reporter.clear_context
23+
raise e
24+
end
25+
end
26+
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
require "abstract_unit"
4+
5+
class ClearEventReporterContextTest < ActiveSupport::TestCase
6+
def setup
7+
@app = ->(env) { [200, {}, ["Hello"]] }
8+
@middleware = ActionDispatch::ClearEventReporterContext.new(@app)
9+
@reporter = ActiveSupport.event_reporter
10+
end
11+
12+
test "clears event reporter context in response finished callback" do
13+
@reporter.set_context(shop_id: 123)
14+
15+
env = {}
16+
@middleware.call(env)
17+
18+
assert env["rack.response_finished"]
19+
assert_equal 1, env["rack.response_finished"].length
20+
21+
env["rack.response_finished"].each(&:call)
22+
23+
assert_equal({}, @reporter.context)
24+
end
25+
26+
test "clears event reporter context when exception is raised" do
27+
@reporter.set_context(shop_id: 123)
28+
29+
exception_app = ->(env) { raise StandardError, "Test exception" }
30+
exception_middleware = ActionDispatch::ClearEventReporterContext.new(exception_app)
31+
32+
env = {}
33+
assert_raises(StandardError) do
34+
exception_middleware.call(env)
35+
end
36+
37+
assert_equal({}, @reporter.context)
38+
end
39+
end

activejob/lib/active_job/railtie.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ class Railtie < Rails::Railtie # :nodoc:
9898
end
9999
end
100100

101+
initializer "active_job.clear_event_reporter_context" do
102+
ActiveSupport.on_load(:active_job) do
103+
ActiveJob::Callbacks.singleton_class.set_callback(:execute, :around) do |_, inner|
104+
inner.call
105+
ensure
106+
ActiveSupport.event_reporter.clear_context
107+
end
108+
end
109+
end
110+
101111
initializer "active_job.query_log_tags" do |app|
102112
query_logs_tags_enabled = app.config.respond_to?(:active_record) &&
103113
app.config.active_record.query_log_tags_enabled &&

activesupport/CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,39 @@
1+
* Add Structured Event Reporter, accessible via `Rails.event`.
2+
3+
The Event Reporter provides a unified interface for producing structured events in Rails
4+
applications:
5+
6+
```ruby
7+
Rails.event.notify("user.signup", user_id: 123, email: "[email protected]")
8+
```
9+
10+
It supports adding tags and context to events:
11+
12+
```ruby
13+
Rails.event.set_context(request_id: "abc123", shop_id: 456)
14+
# All events will contain context: {request_id: "abc123", shop_id: 456}
15+
16+
Rails.event.tagged("graphql") do
17+
Rails.event.notify("user.signup", user_id: 123, email: "[email protected]")
18+
end
19+
# Event includes tags: { graphql: true }
20+
```
21+
22+
Events are emitted to subscribers. Applications may register their own subscribers
23+
to control how events are serialized and emitted.
24+
25+
```ruby
26+
class MySubscriber
27+
def emit(event)
28+
# Serialize event and export to logging platform
29+
end
30+
end
31+
32+
Rails.event.subscribe(MySubscriber.new)
33+
```
34+
35+
*Adrianna Chang*
36+
137
* Given an array of `Thread::Backtrace::Location` objects, the new method
238
`ActiveSupport::BacktraceCleaner#clean_locations` returns an array with the
339
clean ones:

activesupport/lib/active_support.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ module ActiveSupport
4747
autoload :ExecutionWrapper
4848
autoload :Executor
4949
autoload :ErrorReporter
50+
autoload :EventReporter
5051
autoload :FileUpdateChecker
5152
autoload :EventedFileUpdateChecker
5253
autoload :ForkTracker
@@ -109,6 +110,9 @@ def self.eager_load!
109110
@error_reporter = ActiveSupport::ErrorReporter.new
110111
singleton_class.attr_accessor :error_reporter # :nodoc:
111112

113+
@event_reporter = ActiveSupport::EventReporter.new
114+
singleton_class.attr_accessor :event_reporter # :nodoc:
115+
112116
def self.cache_format_version
113117
Cache.format_version
114118
end

0 commit comments

Comments
 (0)