Wide Events
One wide telemetry event per Rails request or job execution, in a database you own.
Wide Events collects everything your app knows about each unit of work (route, user, account, build SHA, query counts, cache hits, feature flags, phase timings, errors) into one flat, high-cardinality event on the OpenTelemetry root span you already export. Every request becomes one row. Every question becomes one query against storage you run: ClickHouse with HyperDX on top is a proven pairing (both open source), any OTLP backend works, and a log-line sink emits one JSON event per request if you'd rather not run tracing at all.
That format matters more now that agents build with you. Telemetry stops being something humans glance at and becomes something software queries in a loop: an agent that instruments a feature, deploys it, and verifies it in production will hit your observability stack fifty times before lunch. One row per request is the densest way to feed production behavior back into a context window, and owning the storage means the loop has no price per iteration.
Install
# Gemfile
gem "wide_events"
bin/rails generate wide_events:install # initializer + attribute registry + AGENTS.md section
bin/rails generate wide_events:skills # agent skills into .claude/skills/
Wide events turn on wherever OTEL_EXPORTER_OTLP_ENDPOINT is set. Dev and test pay nothing.
What you get per event
The gem records the generic attributes on every request and job:
http.request.id,http.response.status_code,http.route.controller, request body size, parseduser_agent.*db.duration_msandview.duration_ms(Rails' own measurements)stats.postgres_query_count/_duration_msandstats.http_call_count/_duration_ms, rolled up from OTel child spans (scope map is configurable)cache.<prefix>hit/miss booleans, capped per eventjob.class,job.queue,job.queue_latency_ms,job.executions,job.scheduled(Solid Queue recurring detection built in, detector pluggable)error,exception.type,exception.message,uptime_sec,main: true
Your code adds the parts only it knows:
WideEvent.set("report.id" => report.id, "report.format" => "pdf")
WideEvent.phase("pdf_render") { render_pdf } # -> pdf_render.duration_ms
WideEvent.error!(slug: "err-export-source-missing", exception: e, expected: true)
Every call is a safe no-op outside a unit of work and never raises into app code. A telemetry bug cannot fail a request or a job.
"Something is slow" turns into "the reports route is slow for account 4218 on build f3a91c, and it's running 742 queries": one query in HyperDX for you, one SQL call for your agent.
The registry is a schema, not a wiki page
Every attribute is declared in config/wide_event/registry.yml:
report.format:
type: string
set_by: ReportsController#create
pii: none
notes: pdf or csv
The gem registers its own attributes by default; globs (feature_flag.*) cover dynamic families. With config.strict = true in the test environment, the suite records every undeclared attribute it sees, and assert_registered_wide_event_attributes fails on them. rake wide_events:registry:check validates the file; rake wide_events:registry:docs generates the human-readable registry doc from it, so documentation can't drift from reality.
Testing
# test_helper.rb
require "wide_event/test_helper"
class ActiveSupport::TestCase
include WideEvent::TestHelper
end
test "report rendering is instrumented" do
assert_wide_event("pdf_render.duration_ms", "report.format" => "pdf") do
Report.new(format: "pdf").render
end
end
test "job emits one wide event" do
events = capture_wide_events { ExportJob.perform_now }
assert_equal 1, events.length
end
Built for the agent loop
bin/rails generate wide_events:skills installs two agent skills:
- instrumenting-wide-events: the write path. Naming conventions, when to use
setvsphasevserror!, the registry workflow, PII rules, test assertions. - debugging-with-wide-events: the read path. Symptom-to-query workflow against ClickHouse/HyperDX or JSON logs, plus the standing queries worth running:
error = true AND exception.slug IS NULLis a permanent, queryable to-do list of rescues nobody instrumented.
The install generator also appends a wide-events section to AGENTS.md, so every future session knows the instrumentation exists and how to extend it.
Configuration
WideEvent.configure do |config|
config.enabled = ENV["OTEL_EXPORTER_OTLP_ENDPOINT"].present? # default
config.sink = :otel # :otel, :log, or any object responding to flush(attrs)
config.strict = Rails.env.test? # track attributes against the registry
config.max_cache_attrs = 10
config.span_scopes = { # OTel instrumentation scope -> stats.* name
"OpenTelemetry::Instrumentation::PG" => "postgres_query",
"OpenTelemetry::Instrumentation::Net::HTTP" => "http_call"
}
config.scheduled_job_detector = ->(job) { ... } # default detects Solid Queue recurring executions
config.error_handler = ->(exception, message) { ... } # default reports via OpenTelemetry.handle_error
end
The railtie inserts the middleware directly below ActionDispatch::Executor (the executor clears the per-request store, so placement matters), instruments ActiveJob::Base, and wires the span counter and notification subscribers after your initializers run. Without Rails, call WideEvent.install! yourself after configuring the OpenTelemetry SDK.
Conventions
Flat keys, dot namespaces, snake_case leaves. Durations end in _duration_ms, counts in _count, booleans read as assertions, timestamps serialize to RFC 3339. Opaque ids are fine; names, emails, and request params are not, and anything that could quote user input is flagged pii: review in the registry.
Unhandled exceptions get error: true with no slug, deliberately: the missing slug marks the rescue you haven't instrumented yet.
Development
bin/setup # bundle install + appraisal gemfiles
bundle exec rake test # run the suite
bin/rubocop # lint (rubocop-rails-omakase)
The CI matrix runs the suite across Ruby 3.2 to 4.0 and Rails 7.1 to main via Appraisal; run a specific combination locally with BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test. Releases go out with bin/release <version>.
Requirements
Ruby >= 3.2, Rails >= 7.1 (activesupport and rack are the only hard dependencies; opentelemetry-sdk and useragent are optional and detected at runtime).
License
MIT.