Skip to content

Commit 6b18088

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 aca0374 commit 6b18088

File tree

18 files changed

+1725
-0
lines changed

18 files changed

+1725
-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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,44 @@
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 to events:
11+
12+
```ruby
13+
Rails.event.tagged("graphql") do
14+
# Event includes tags: { graphql: true }
15+
Rails.event.notify("user.signup", user_id: 123, email: "[email protected]")
16+
end
17+
```
18+
19+
As well as context:
20+
```ruby
21+
# All events will contain context: {request_id: "abc123", shop_id: 456}
22+
Rails.event.set_context(request_id: "abc123", shop_id: 456)
23+
```
24+
25+
Events are emitted to subscribers. Applications register subscribers to
26+
control how events are serialized and emitted. Rails provides several default
27+
encoders that can be used to serialize events to common formats:
28+
29+
```ruby
30+
class MySubscriber
31+
def emit(event)
32+
encoded_event = ActiveSupport::EventReporter.encoder(:json).encode(event)
33+
StructuredLogExporter.export(encoded_event)
34+
end
35+
end
36+
37+
Rails.event.subscribe(MySubscriber.new)
38+
```
39+
40+
*Adrianna Chang*
41+
142
* Make `ActiveSupport::Gzip.compress` deterministic based on input.
243

344
`ActiveSupport::Gzip.compress` used to include a timestamp in the output,

activesupport/lib/active_support.rb

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

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

0 commit comments

Comments
 (0)