- Notifications
You must be signed in to change notification settings - Fork22.1k
Structured Event Reporting in Rails#55334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
90ba540 toafad46cCompareactionpack/lib/action_dispatch/middleware/clear_event_reporter_context.rb OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
bdewater-thatch left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Hi, thanks for working on this! I've been thinking about how we would use this in our app and I have some questions.
Note that 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.
My first impression was: the interface is similar toActiveSupport::Notifications with the "context stack" fromActiveSupport::ErrorReporter added. I am wondering how you see AS::Notifications overlap/compliment with the proposed events interface. Will Rails internally start using this new interface?
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.
I understand the need for flexibility but the bolded part reads like configuration over convention 😅 I expected a default formatter (that could be disabled, if desired) at least. Something like the below subscriber seems useful in a variety of places:
- the development environment
- simple production logging setups (e.g.
jq 'select(.id == 123)' logs/production.log) when deploying to self-managed servers - stdout log capture in PaaS like Render
classJSONLoggingSubscriberdefemit(event)stringified_event=JSON.dump(event)ifRails.event.debug_mode?Rails.logger.debug(stringified_event)elseRails.logger.info(stringified_event)endendend
Uh oh!
There was an error while loading.Please reload this page.
adrianna-chang-shopify commentedJul 15, 2025
Hi@bdewater-thatch , thanks for the feedback! <3
Yeah, this is a great callout! I view these as complimentary interfaces for sure, with the use case for
Agree with you completely! I think a default formatter that just dumps to JSON and produces an unstructured log absolutely makes sense. (Happy to amend this PR to include something along those lines). My comment was mainly around having the Event Reporter standardize on an event format without concerning itself with how exactly those events are encoded / serialization formats / etc. Or, for example, choosing whether to aggregate events and emit OOB vs emitting synchronously within the request. |
5675e29 to338ecf5Compareadrianna-chang-shopify commentedJul 16, 2025
@zzak and@bdewater-thatch I added338ecf5 with encoders for both JSON and msgpack and updated the language in the docs. Let me know what you think :) The next step IMO would be to then add a default subscriber along the lines of what@bdewater-thatch mentioned, which could serialize using the JSON encoder and then called the unstructured logger. |
activesupport/lib/active_support/testing/event_reporter_assertions.rb OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
338ecf5 tof5d9c02Compareadrianna-chang-shopify commentedJul 18, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Pushed changes related to Still to-do
|
activesupport/lib/active_support/testing/event_reporter_assertions.rb OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
a320b9f toa266aefCompareactionpack/lib/action_dispatch/middleware/clear_event_reporter_context.rb OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
| classMessagePack <Base | ||
| defself.encode(event) | ||
| require"msgpack" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I imagine event reporting wouldn't betoo hot, but I think it would be best if we move the require up to configuration time. In addition, that would help users figure out they may be missing a gem during boot instead of randomly during runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Makes sense! 🙇♀️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I put together some ideas inShopify#40, based on something likeActiveSupport::Messages::SerializerWithFallback.[](format):
| defself.[](format) | |
| ifformat.to_s.include?("message_pack") && !defined?(ActiveSupport::MessagePack) | |
| require"active_support/message_pack" | |
| end | |
| SERIALIZERS.fetch(format) |
adrianna-chang-shopifyJul 28, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Yeah, I think this makes sense! Maybe we can document an example like this for users:
classLogSubscriberdefinitialize@encoder=ActiveSupport::EventReporter.encoder(:json)enddefemit(event)encoded_data=@encoder.encode(event)Rails.logger.info(encoded_data)endendinitializer"event_reporter_setup"doRails.event.subscribe(LogSubscriber.new)end
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
adrianna-chang-shopify commentedAug 12, 2025
Hey@palkan -- yeah, that PR definitely needs a bit of cleaning up ahead of being opened. Mostly wanted to showcase the direction we could move in for native structured event reporting in the Rails libraries (and verify that Action Pack / Active Job work with the way we're resetting context for the Event Reporter in this PR). I'll definitely take a look at your feedback more in depth when we get ready to bring that in officially. You raise great points (duplication between the subscribers was something I'd noted as well).
@zzak has been working on filtering params (Shopify#37,Shopify#38), probably best to land that first. Although IIRC sensitive data is already scrubbed e.g. from the request before it hits the log subscribers? We should verify. @rafaelfranca let me know if anything else is missing on your end from this initial PR. I'd like to work on more extensive documentation / additions to the guides, and those event subscribers in a follow-up. And@zzak has some work on top of this as well ❤️ |
947cc00 to2731985Compare| initializer"active_support.set_event_reporter_context_store"do |app| | ||
| config.after_initializedo | ||
| ifklass=app.config.active_support.event_reporter_context_store |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Missing documentation for this config in the configuring guide.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Done:
rails/guides/source/configuring.md
Lines 2955 to 2984 in071670b
| #### `config.active_support.event_reporter_context_store` | |
| Configures a custom context storefor theEventReporter.The context store is used to manage metadata that should be attached to every event emitted by the reporter. | |
| By default, theEventReporter uses`ActiveSupport::EventContext` which stores contextin fiber-local storage. | |
| To use a custom context store, set this config to aclass that implements the context store interface: | |
| ```ruby | |
| # config/application.rb | |
| config.active_support.event_reporter_context_store = CustomContextStore | |
| class CustomContextStore | |
| class << self | |
| def context | |
| # Return the context hash | |
| end | |
| def set_context(context_hash) | |
| # Append context_hash to the existing context store | |
| end | |
| def clear | |
| # Clear the stored context | |
| end | |
| end | |
| end | |
| ``` | |
| Defaults to`nil`, which means the default`ActiveSupport::EventContext` store is used. |
Ref:rails#50452This 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 metricsand logs. The Event Reporter is designed to be a single interface for producing any kind of event in a Railsapplication.We separate the emission of events from how these events reach end consumers; applications are expected todefine their own subscribers, and the Event Reporter is responsible for emitting events to these subscribers.
2731985 to071670bCompare1037508 intorails:mainUh oh!
There was an error while loading.Please reload this page.
When `ActiveSupport::EventReporter.encoder(:msgpack)` is called, typically during boot, we can catch when the user forgot to add the gem and give them a warning.This is similar to how `ActiveSupport::Messages::SerializerWithFallback.[](format)` worksSee:https://github.com/rails/rails/blob/6f912a5986cd9295e3fbb45b1ec22159e51ad65d/activesupport/lib/active_support/messages/serializer_with_fallback.rb#L9-L14Context:rails#55334 (comment)
When `ActiveSupport::EventReporter.encoder(:msgpack)` is called, typically during boot, we can catch when the user forgot to add the gem and give them a warning.This is similar to how `ActiveSupport::Messages::SerializerWithFallback.[](format)` worksSee:https://github.com/rails/rails/blob/6f912a5986cd9295e3fbb45b1ec22159e51ad65d/activesupport/lib/active_support/messages/serializer_with_fallback.rb#L9-L14Also `ActiveSupport::MessagePack`:https://github.com/rails/rails/blob/2ca26346a563a375277e97a09d879df33682df55/activesupport/lib/active_support/message_pack.rb#L3-L10Context:rails#55334 (comment)
When `ActiveSupport::EventReporter.encoder(:msgpack)` is called, typically during boot, we can catch when the user forgot to add the gem and give them a warning.This is similar to how `ActiveSupport::Messages::SerializerWithFallback.[](format)` worksSee:https://github.com/rails/rails/blob/6f912a5986cd9295e3fbb45b1ec22159e51ad65d/activesupport/lib/active_support/messages/serializer_with_fallback.rb#L9-L14Also `ActiveSupport::MessagePack`:https://github.com/rails/rails/blob/2ca26346a563a375277e97a09d879df33682df55/activesupport/lib/active_support/message_pack.rb#L3-L10Context:rails#55334 (comment)
When `ActiveSupport::EventReporter.encoder(:msgpack)` is called, typically during boot, we can catch when the user forgot to add the gem and give them a warning.This is similar to how `ActiveSupport::Messages::SerializerWithFallback.[](format)` worksSee:https://github.com/rails/rails/blob/6f912a5986cd9295e3fbb45b1ec22159e51ad65d/activesupport/lib/active_support/messages/serializer_with_fallback.rb#L9-L14Also `ActiveSupport::MessagePack`:https://github.com/rails/rails/blob/2ca26346a563a375277e97a09d879df33682df55/activesupport/lib/active_support/message_pack.rb#L3-L10Context:rails#55334 (comment)
When `ActiveSupport::EventReporter.encoder(:msgpack)` is called, typically during boot, we can catch when the user forgot to add the gem and give them a warning.This is similar to how `ActiveSupport::Messages::SerializerWithFallback.[](format)` worksSee:https://github.com/rails/rails/blob/6f912a5986cd9295e3fbb45b1ec22159e51ad65d/activesupport/lib/active_support/messages/serializer_with_fallback.rb#L9-L14Also `ActiveSupport::MessagePack`:https://github.com/rails/rails/blob/2ca26346a563a375277e97a09d879df33682df55/activesupport/lib/active_support/message_pack.rb#L3-L10Context:rails#55334 (comment)
When `ActiveSupport::EventReporter.encoder(:msgpack)` is called, typically during boot, we can catch when the user forgot to add the gem and give them a warning.This is similar to how `ActiveSupport::Messages::SerializerWithFallback.[](format)` worksSee:https://github.com/rails/rails/blob/6f912a5986cd9295e3fbb45b1ec22159e51ad65d/activesupport/lib/active_support/messages/serializer_with_fallback.rb#L9-L14Also `ActiveSupport::MessagePack`:https://github.com/rails/rails/blob/2ca26346a563a375277e97a09d879df33682df55/activesupport/lib/active_support/message_pack.rb#L3-L10Context:rails#55334 (comment)
| warn(<<~MESSAGE) | ||
| Event reporter subscriber#{subscriber.class.name} raised an error on #emit:#{subscriber_error.message} | ||
| #{subscriber_error.backtrace&.join("\n")} | ||
| MESSAGE |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
We got error reporter for that now.
| # Example usage in a subscriber: | ||
| # | ||
| # class LogSubscriber | ||
| # def emit(event) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Apologies for commenting on a merged pull request, but… is “emit” the correct word to use here? I would expect an#emit method to be the one emitting the event to notify a subscriber. Would a word like “capture” or “handle” better describe what the method does?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This confused me too when I was first reading through the latest beta release notes; but on further thought, I think that it's because the subscriber itself is also generally expected toemit the event that it consumed once it has shaped it, so the subscribers are kind of acting as serializers for the structured event data they're about toemit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Yes.
Subscribers must implement the
emitmethod, which will be called with the event hash.
Does that clear up your concerns?
If you try to subscribe with an object that doesn't respond toemit you will get an error, that might be an opportunity to improve the DX but since it's documented felt out of scope IMO.
Uh oh!
There was an error while loading.Please reload this page.
Motivation / Background
Ref:#50452
This PR adds structured event reporting to Rails. The event reporter, accessible via
Rails.event, allows you to report structured events to subscriber(s), and provides mechanisms for adding tags and context to events. While the existingRails.loggeris great for producing human-readable logs, the unstructured log lines it generates are difficult for modern data analytics / observability platforms to parse. We'd like to add first-class support for structured events, complementing the existing Rails logger.Note that 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.
This is the API we developed for structured logging at Shopify and are using in our monolith.
Detail
Basic Event Reporting
Event Structure
Adding Tags / Context
Debug Mode - Conditional event reporting:
Subscriber Interface
EDIT: I've amended this PR to provide some default encoders out of the box, and we may additionally want to provide a default subscriber that converts structured events into unstructured log lines.
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.
Testing Support
Key Design Decisions
Additional information
What is an event?
"Business Events", Traces, Spans, Metrics and Logs are all types of events. At Shopify, we built the Structured Event Reporter to be a unified solution for any and all of these use cases.
Why support arbitrarily structured payloads (hashes) and "event objects"?
It is important for the API to support both implicitly structured events (hashes) and explicitly typed events that comply with a schema. At Shopify, we're moving towards a future where all our events are schematized. The Event Reporter allows "event objects" to be passed to the Event Reporter, and these can have defined schemas. These objects are passed directly to subscribers in the
payloadfield; we don't make any assumptions about how these should be serialized. The subscriber(s) need to handle encoding these appropriately.Why no log levels?
Log levels are rather arbitrary. Different logging systems use different levels, and these are interpreted differently depending on the user / context and are generally used inconsistently. The primary use case for log levels is to distinguish:
(Source:https://dave.cheney.net/2015/11/05/lets-talk-about-logging)
As such, when designing this API, we opted to implicitly support "log levels" in the API via
Rails.event.notifyandRails.event.debug. We don't require additional level parameters, nor do we expose different levels in the API.How do the payload, tags, and context differ?
Thepayload, or event object, contains information about an event that occurred, typically specific to a given domain.Tags can be used to enrich an event with further context, and form a "stack"; all events within the block will include tags currently pushed onto the stack. Note that tags and the event payload may occupy logically different domains. Thecontext is designed for request/job-level metadata that spans the entire execution context. Context should expand over the course of the unit of work, and will be attached to every event emitted by the event reporter.
Why are we not using
ActiveSupport::ExecutionContextfor the event context?At Shopify, we specifically want to use Fiber storage as the underlying storage mechanism for storing event reporter context. Weexplored making changes to the existing execution context / isolated execution state singletons to allow this, but we decided it made more sense to handroll what we needed specifically for the event reporter, and then revisit folding this back into AS::EC / AS::ISE later.
Checklist
Before submitting the PR make sure the following are checked:
[Fix #issue-number]