Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Merged
rafaelfranca merged 1 commit intorails:mainfromShopify:ac-structured-events
Aug 13, 2025

Conversation

@adrianna-chang-shopify
Copy link
Contributor

@adrianna-chang-shopifyadrianna-chang-shopify commentedJul 14, 2025
edited
Loading

Motivation / Background

Ref:#50452

This PR adds structured event reporting to Rails. The event reporter, accessible viaRails.event, allows you to report structured events to subscriber(s), and provides mechanisms for adding tags and context to events. While the existingRails.logger is 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

# Simple event with payloadRails.event.notify("user_created",{id:123,name:"John Doe"})# Event object (for schematized events)Rails.event.notify(UserCreatedEvent.new(id:123,name:"John Doe"))

Event Structure

# All events include standardized metadata:{name:"user_created",payload:{id:123,name:"John Doe"},tags:{section:"admin"},context:{request_id:"abc123",user_agent:"Mozilla..."},timestamp:1738964843208679035,# nanosecond precisionsource_location:{filepath:"app/services/user_service.rb",lineno:45,label:"UserService#create"}}

Adding Tags / Context

# Tags - Domain-specific context that nests:Rails.event.tagged("graphql")doRails.event.tagged(section:"admin")doRails.event.notify("user_created",{id:123})# Event includes tags: { graphql: true, section: "admin" }endend# Context - Request/job-scoped metadata:Rails.event.set_context(request_id:"abc123",shop_id:456)Rails.event.notify("user_created",{id:123})# All subsequent events in this request include the context

Debug Mode - Conditional event reporting:

Rails.event.with_debugdoRails.event.debug("sql_query",{sql:"SELECT * FROM users"})# Only reported when debug mode is activeend

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.

classMySubscriberdefemit(event)# Process the structured event hash# Serialize and export to logging pipelineendendRails.event.subscribe(MySubscriber.new)

Testing Support

# Assert specific events are reportedassert_event_reported("user.created",payload:{id:123})doUserService.create_user(name:"John")end# Assert no events are reportedassert_no_event_reported("user.deleted")doUserService.update_user(id:123,name:"Jane")end

Key Design Decisions

  1. Fiber-based Isolation: Tags and context are scoped per fiber. Child fibers / threads inherit context from the parent, but the context is copied on write so that changes do not propagate to the parent.
  2. Structured Metadata: Every event includes consistent name, timestamp, source location, payload, tags, and context.
  3. Flexible Payloads: Supports both hash payloads and custom event objects for implicitly structured events and explicitly schematized events.
  4. Publisher-Subscriber: Decouples event emission from processing, allowing multiple subscribers and providing flexibility for applications to define how events reach consumers.

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 thepayload field; 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:

Things that developers care about when they are developing or debugging software.
Things that users care about when using your software.

Obviously these are debug and info levels, respectively.

(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 viaRails.event.notify andRails.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 usingActiveSupport::ExecutionContext for 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:

  • This Pull Request is related to one change. Unrelated changes should be opened in separate PRs.
  • Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex:[Fix #issue-number]
  • Tests are added or updated if you fix a bug or add a feature.
  • CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included.

robertlaurin, Philwi, and rosa reacted with hooray emojimarcelolx, johannesschobel, fractaledmind, andreimaxim, tmatti, rodloboz, thomasklemm, zzak, ogirginc, schpet, and 76 more reacted with heart emojimarcelolx, vipulnsward, rodloboz, thomasklemm, zzak, joaomarcos96, lagr, Deeptwix, alex-damian-negru, pedro-stanaka, and 26 more reacted with rocket emoji
Copy link

@bdewater-thatchbdewater-thatch left a 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

@adrianna-chang-shopify
Copy link
ContributorAuthor

Hi@bdewater-thatch , thanks for the feedback! <3

I am wondering how you see AS::Notifications overlap/compliment with the proposed events interface. Will Rails internally start using this new interface?

Yeah, this is a great callout! I view these as complimentary interfaces for sure, with the use case forActiveSupport::Notifications.{instrument|subscribe} being different from the use case forRails.event. ASN is primarily for instrumentation and performance monitoring. It has duration (start,finish) and performance-related metadata. You can, of course, instrument point-in-time events with Active Support notifications, but thesubscribe /render API is a little clunky to do so.Rails.event is intended to be for richly contextualized event emission, supporting features like tags and schematized events. I don't see any of the ASN events Rails provides going away; these continue to be useful hook points for applications to subscribe to. ASN events are currently converted to unstructured log lines via theLogSubscribers, and Ido see us beginning to emit real structured events / logs for all of said hook points.

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.

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.

@adrianna-chang-shopifyadrianna-chang-shopifyforce-pushed theac-structured-events branch 3 times, most recently from5675e29 to338ecf5CompareJuly 16, 2025 20:37
@adrianna-chang-shopify
Copy link
ContributorAuthor

@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.

@adrianna-chang-shopify
Copy link
ContributorAuthor

adrianna-chang-shopify commentedJul 18, 2025
edited
Loading

Pushed changes related toHashWithIndifferentAccess

Still to-do

@adrianna-chang-shopifyadrianna-chang-shopifyforce-pushed theac-structured-events branch 2 times, most recently froma320b9f toa266aefCompareJuly 22, 2025 15:06

classMessagePack <Base
defself.encode(event)
require"msgpack"
Copy link
Member

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.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Makes sense! 🙇‍♀️

Copy link
Member

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-shopify reacted with heart emoji
Copy link
ContributorAuthor

@adrianna-chang-shopifyadrianna-chang-shopifyJul 28, 2025
edited
Loading

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

@adrianna-chang-shopify
Copy link
ContributorAuthor

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

Adding params to the event payload raises a question of security: should events contain potentially sensitive information? Do we need some kind of filtering or we should avoid emitting such data?

@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 ❤️

zzak reacted with thumbs up emojialexcameron89 reacted with hooray emojipalkan and alexcameron89 reacted with heart emoji


initializer"active_support.set_event_reporter_context_store"do |app|
config.after_initializedo
ifklass=app.config.active_support.event_reporter_context_store
Copy link
Member

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.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Done:

#### `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.
@rafaelfrancarafaelfranca merged commit1037508 intorails:mainAug 13, 2025
3 checks passed
zzak added a commit to zzak/rails that referenced this pull requestAug 14, 2025
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)
zzak added a commit to zzak/rails that referenced this pull requestAug 14, 2025
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)
zzak added a commit to zzak/rails that referenced this pull requestAug 14, 2025
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)
zzak added a commit to zzak/rails that referenced this pull requestAug 14, 2025
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)
zzak added a commit to zzak/rails that referenced this pull requestAug 14, 2025
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)
zzak added a commit to zzak/rails that referenced this pull requestAug 15, 2025
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)
Comment on lines +365 to +368
warn(<<~MESSAGE)
Event reporter subscriber#{subscriber.class.name} raised an error on #emit:#{subscriber_error.message}
#{subscriber_error.backtrace&.join("\n")}
MESSAGE
Copy link
Member

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)
Copy link
Contributor

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?

AgentAntelope, razumau, and ohmycthulhu reacted with thumbs up emoji

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Yes.

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

https://edgeapi.rubyonrails.org/classes/ActiveSupport/EventReporter.html#class-ActiveSupport::EventReporter-label-Subscribers

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.

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@Edouard-chinEdouard-chinEdouard-chin left review comments

@byrootbyrootbyroot left review comments

@zzakzzakzzak left review comments

@skipkayhilskipkayhilskipkayhil left review comments

@rafaelfrancarafaelfrancarafaelfranca approved these changes

+9 more reviewers

@PhilCogginsPhilCogginsPhilCoggins left review comments

@bdewater-thatchbdewater-thatchbdewater-thatch left review comments

@AgentAntelopeAgentAntelopeAgentAntelope left review comments

@ioquatixioquatixioquatix left review comments

@alexcameron89alexcameron89alexcameron89 left review comments

@viktorianerviktorianerviktorianer left review comments

@bquorningbquorningbquorning left review comments

@that-jillthat-jillthat-jill left review comments

@george-mageorge-mageorge-ma left review comments

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

20 participants

@adrianna-chang-shopify@ioquatix@kigster@zzak@palkan@rafaelfranca@rsamoilov@Kerrick@KieranP@bquorning@byroot@AgentAntelope@PhilCoggins@alexcameron89@skipkayhil@Edouard-chin@viktorianer@that-jill@george-ma@bdewater-thatch

[8]ページ先頭

©2009-2025 Movatter.jp