wide_events 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +125 -15
- data/lib/generators/wide_events/install/install_generator.rb +17 -0
- data/lib/wide_event/railtie.rb +9 -1
- data/lib/wide_event/registry.rb +5 -0
- data/lib/wide_event/test_helper.rb +9 -2
- data/lib/wide_event/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c53fd3a4d33b817af708d0675bfa0b8047e2487a354d62cfb9e3c915b9c3472
|
|
4
|
+
data.tar.gz: 747298c60ca63d3a90db33abbf69d430e20a67d5037f79dcbf6d018ab5c86cb4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d54b65bd07d6294109efb49020299da91aeb040000c42841a175f1b7af3e7fb5a6989971fee3fae5a57f8c0d2c5692572cd08bd4fb978c5798cb7ce9ce98783f
|
|
7
|
+
data.tar.gz: e2a3a0d2d6400b1c1cd951c3f6c71c059ecc0cd2b67342beba43901a8ca30929b98ecafe5cab47994e0c3a5153b9773f186670a73ef7eb13894fff40bd9055f3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.1.2 (2026-08-03)
|
|
4
|
+
|
|
5
|
+
- README "In production" section and `docs/production-example.md`: an
|
|
6
|
+
anonymized composite event, instrumentation patterns, and standing
|
|
7
|
+
queries from the production app the gem was extracted from.
|
|
8
|
+
|
|
9
|
+
## v0.1.1 (2026-08-03)
|
|
10
|
+
|
|
11
|
+
- The install generator wires `WideEvent::TestHelper` into
|
|
12
|
+
`test/test_helper.rb` (or prints manual instructions when the file
|
|
13
|
+
isn't recognized).
|
|
14
|
+
- Development boot logs a one-time notice when wide events are disabled,
|
|
15
|
+
pointing at the `:log` sink quickstart.
|
|
16
|
+
- `assert_registered_wide_event_attributes` resets the violation tally
|
|
17
|
+
after each check, so it can run as a global `teardown` and works with
|
|
18
|
+
parallel test workers.
|
|
19
|
+
- README rewritten around first-run experience: 60-second `:log` sink
|
|
20
|
+
quickstart, OpenTelemetry setup guide, enablement modes, end-to-end
|
|
21
|
+
registry workflow, log sink operations, request-vs-job event semantics.
|
|
22
|
+
- New `docs/clickhouse-hyperdx.md`: local quickstart and query cookbook.
|
|
23
|
+
|
|
3
24
|
## v0.1.0 (2026-08-03)
|
|
4
25
|
|
|
5
26
|
First release, extracted from a production Rails app.
|
data/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Wide Events
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/wide_events)
|
|
4
|
+
[](https://github.com/adammiribyan/wide_events/actions/workflows/ci.yml)
|
|
5
|
+
|
|
3
6
|
One wide telemetry event per Rails request or job execution, in a database you own.
|
|
4
7
|
|
|
5
8
|
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.
|
|
@@ -11,18 +14,70 @@ That format matters more now that agents build with you. Telemetry stops being s
|
|
|
11
14
|
```ruby
|
|
12
15
|
# Gemfile
|
|
13
16
|
gem "wide_events"
|
|
17
|
+
gem "useragent" # optional: parses user_agent.browser/os/platform from the raw UA string
|
|
14
18
|
```
|
|
15
19
|
|
|
16
20
|
```bash
|
|
17
|
-
bin/rails generate wide_events:install # initializer + attribute registry + AGENTS.md section
|
|
21
|
+
bin/rails generate wide_events:install # initializer + attribute registry + AGENTS.md section + test helper wiring
|
|
18
22
|
bin/rails generate wide_events:skills # agent skills into .claude/skills/
|
|
19
23
|
```
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
## See an event in 60 seconds
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
Wide events are off unless `OTEL_EXPORTER_OTLP_ENDPOINT` is set (the development log tells you so at boot). To see one immediately, no tracing required, enable the log sink in `config/initializers/wide_events.rb`:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
WideEvent.configure do |config|
|
|
31
|
+
config.enabled = true
|
|
32
|
+
config.sink = :log
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Hit any route, then:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
grep '"main":true' log/development.log
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
One JSON object per request: route, status, timings, query counts, everything.
|
|
43
|
+
|
|
44
|
+
## Production setup with OpenTelemetry
|
|
45
|
+
|
|
46
|
+
The default sink writes the event onto the current OTel root span, which is the span OTel's Rack instrumentation opens at the top of the middleware stack. That means production needs an OpenTelemetry SDK configured; the gem detects it and wires itself after your initializers run. Minimal setup:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# Gemfile
|
|
50
|
+
gem "opentelemetry-sdk"
|
|
51
|
+
gem "opentelemetry-exporter-otlp"
|
|
52
|
+
gem "opentelemetry-instrumentation-rails"
|
|
53
|
+
gem "opentelemetry-instrumentation-rack"
|
|
54
|
+
gem "opentelemetry-instrumentation-pg" # enables stats.postgres_query_*
|
|
55
|
+
gem "opentelemetry-instrumentation-net_http" # enables stats.http_call_*
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# config/initializers/opentelemetry.rb
|
|
60
|
+
return if ENV["OTEL_EXPORTER_OTLP_ENDPOINT"].to_s.empty?
|
|
61
|
+
|
|
62
|
+
require "opentelemetry/sdk"
|
|
63
|
+
require "opentelemetry/exporter/otlp"
|
|
64
|
+
require "opentelemetry/instrumentation/rails"
|
|
65
|
+
require "opentelemetry/instrumentation/rack"
|
|
66
|
+
require "opentelemetry/instrumentation/pg"
|
|
67
|
+
require "opentelemetry/instrumentation/net/http"
|
|
68
|
+
|
|
69
|
+
OpenTelemetry::SDK.configure(&:use_all)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 # your collector or backend
|
|
74
|
+
OTEL_SERVICE_NAME=myapp
|
|
75
|
+
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production
|
|
76
|
+
```
|
|
24
77
|
|
|
25
|
-
|
|
78
|
+
With the endpoint set, wide events enable themselves: the railtie inserts the middleware directly below `ActionDispatch::Executor` (the executor clears the per-request store, so placement matters), instruments `ActiveJob::Base`, and registers the span counter and notification subscribers via `WideEvent.install!` after your initializers run. Without Rails, call `WideEvent.install!` yourself after configuring the SDK.
|
|
79
|
+
|
|
80
|
+
## What you get per event
|
|
26
81
|
|
|
27
82
|
- `http.request.id`, `http.response.status_code`, `http.route.controller`, request body size, parsed `user_agent.*`
|
|
28
83
|
- `db.duration_ms` and `view.duration_ms` (Rails' own measurements)
|
|
@@ -31,21 +86,32 @@ The gem records the generic attributes on every request and job:
|
|
|
31
86
|
- `job.class`, `job.queue`, `job.queue_latency_ms`, `job.executions`, `job.scheduled` (Solid Queue recurring detection built in, detector pluggable)
|
|
32
87
|
- `error`, `exception.type`, `exception.message`, `uptime_sec`, `main: true`
|
|
33
88
|
|
|
34
|
-
|
|
89
|
+
A request and the jobs it enqueues are separate units of work: the request's wide event records the enqueue inside its timings, and each job execution emits its own event carrying `job.queue_latency_ms` (time spent waiting in the queue), so queue pressure is visible per job, not smeared into request latency.
|
|
90
|
+
|
|
91
|
+
## Adding your own attributes
|
|
35
92
|
|
|
36
93
|
```ruby
|
|
37
94
|
WideEvent.set("report.id" => report.id, "report.format" => "pdf")
|
|
38
95
|
WideEvent.phase("pdf_render") { render_pdf } # -> pdf_render.duration_ms
|
|
39
|
-
WideEvent.error!(slug: "err-export-source-missing", exception: e, expected: true)
|
|
40
96
|
```
|
|
41
97
|
|
|
42
98
|
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.
|
|
43
99
|
|
|
44
|
-
|
|
100
|
+
Errors come in two shapes, and the difference is the point:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# Handled: you rescued it, you name it
|
|
104
|
+
rescue Vendor::ApiError => e
|
|
105
|
+
WideEvent.error!(slug: "err-vendor-sync-failed", exception: e, expected: true)
|
|
106
|
+
|
|
107
|
+
# Unhandled: any exception that escapes gets error: true with NO slug, automatically
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
That makes `error = true AND exception.slug IS NULL` a standing query: every row is a failure nobody wrote a rescue for, which is a permanent, prioritized to-do list of rescues worth instrumenting.
|
|
45
111
|
|
|
46
112
|
## The registry is a schema, not a wiki page
|
|
47
113
|
|
|
48
|
-
Every attribute is declared in `config/wide_event/registry.yml
|
|
114
|
+
Every attribute is declared in `config/wide_event/registry.yml` (the gem's own attributes are pre-registered; globs like `feature_flag.*` cover dynamic families):
|
|
49
115
|
|
|
50
116
|
```yaml
|
|
51
117
|
report.format:
|
|
@@ -55,15 +121,30 @@ report.format:
|
|
|
55
121
|
notes: pdf or csv
|
|
56
122
|
```
|
|
57
123
|
|
|
58
|
-
The
|
|
124
|
+
The workflow, end to end:
|
|
125
|
+
|
|
126
|
+
1. Set the attribute in code, declare it in `registry.yml`, same change.
|
|
127
|
+
2. Assert it in a test with `assert_wide_event`.
|
|
128
|
+
3. Strict mode (`config.strict = Rails.env.test?`, the generated default) records any undeclared attribute the suite sees; `assert_registered_wide_event_attributes` fails on them.
|
|
129
|
+
4. CI runs `bin/rails wide_events:registry:check` to validate the file.
|
|
130
|
+
5. `bin/rails wide_events:registry:docs` generates `docs/wide-events-registry.md` from it, so documentation can't drift from reality.
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
# .github/workflows/ci.yml (host app)
|
|
134
|
+
- run: bin/rails wide_events:registry:check
|
|
135
|
+
- run: bin/rails test
|
|
136
|
+
```
|
|
59
137
|
|
|
60
138
|
## Testing
|
|
61
139
|
|
|
140
|
+
The install generator wires this into `test/test_helper.rb` (or tells you how):
|
|
141
|
+
|
|
62
142
|
```ruby
|
|
63
|
-
# test_helper.rb
|
|
64
143
|
require "wide_event/test_helper"
|
|
144
|
+
|
|
65
145
|
class ActiveSupport::TestCase
|
|
66
146
|
include WideEvent::TestHelper
|
|
147
|
+
teardown { assert_registered_wide_event_attributes }
|
|
67
148
|
end
|
|
68
149
|
```
|
|
69
150
|
|
|
@@ -80,14 +161,26 @@ test "job emits one wide event" do
|
|
|
80
161
|
end
|
|
81
162
|
```
|
|
82
163
|
|
|
164
|
+
The teardown pattern makes the registry check work with parallel test workers: violation tracking is per process, and checking (then resetting) after every test means the test that set an undeclared attribute is the one that fails, in whichever worker it ran.
|
|
165
|
+
|
|
166
|
+
## When events are emitted
|
|
167
|
+
|
|
168
|
+
| Environment | Typical setup | Result |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| Production / staging | `OTEL_EXPORTER_OTLP_ENDPOINT` set | Enabled automatically, `:otel` sink, events on root spans |
|
|
171
|
+
| Development | `config.enabled = true`, `config.sink = :log` | One JSON line per request in the log |
|
|
172
|
+
| Test | Enabled via the test helper's capture, or strict mode for registry tracking | `capture_wide_events` collects them |
|
|
173
|
+
|
|
174
|
+
`WideEvent.install!` runs once at boot, only if enabled at that point. Flipping `config.enabled` at runtime gates the middleware and job hook (they check per request), but the span counter and subscribers only register at boot, so treat enablement as a boot-time decision.
|
|
175
|
+
|
|
83
176
|
## Built for the agent loop
|
|
84
177
|
|
|
85
178
|
`bin/rails generate wide_events:skills` installs two agent skills:
|
|
86
179
|
|
|
87
180
|
- **instrumenting-wide-events**: the write path. Naming conventions, when to use `set` vs `phase` vs `error!`, the registry workflow, PII rules, test assertions.
|
|
88
|
-
- **debugging-with-wide-events**: the read path. Symptom-to-query workflow against ClickHouse/HyperDX or JSON logs, plus the standing queries worth running
|
|
181
|
+
- **debugging-with-wide-events**: the read path. Symptom-to-query workflow against ClickHouse/HyperDX or JSON logs, plus the standing queries worth running.
|
|
89
182
|
|
|
90
|
-
|
|
183
|
+
Skills land in `.claude/skills/`, which Claude Code and Claude-compatible agents read. Other agents (Cursor, etc.) read the pointer the install generator appends to `AGENTS.md`; the SKILL.md files are plain markdown, so copy or symlink them wherever your tooling looks (`.cursor/rules/`, docs, a system prompt).
|
|
91
184
|
|
|
92
185
|
## Configuration
|
|
93
186
|
|
|
@@ -95,6 +188,7 @@ The install generator also appends a wide-events section to `AGENTS.md`, so ever
|
|
|
95
188
|
WideEvent.configure do |config|
|
|
96
189
|
config.enabled = ENV["OTEL_EXPORTER_OTLP_ENDPOINT"].present? # default
|
|
97
190
|
config.sink = :otel # :otel, :log, or any object responding to flush(attrs)
|
|
191
|
+
config.logger = nil # :log sink target; defaults to Rails.logger
|
|
98
192
|
config.strict = Rails.env.test? # track attributes against the registry
|
|
99
193
|
config.max_cache_attrs = 10
|
|
100
194
|
config.span_scopes = { # OTel instrumentation scope -> stats.* name
|
|
@@ -106,14 +200,30 @@ WideEvent.configure do |config|
|
|
|
106
200
|
end
|
|
107
201
|
```
|
|
108
202
|
|
|
109
|
-
|
|
203
|
+
Log sink notes: events go to `config.logger` (or `Rails.logger`) at info level as single-line JSON. Point it at a dedicated logger (`config.logger = Logger.new("log/wide_events.log")`) to keep them out of your main log, and leave the sink off in the test environment unless you want one JSON line per test request; `capture_wide_events` is the intended test-side tap.
|
|
204
|
+
|
|
205
|
+
## Querying: ClickHouse + HyperDX
|
|
206
|
+
|
|
207
|
+
[docs/clickhouse-hyperdx.md](docs/clickhouse-hyperdx.md) has a local quickstart (one container) and a query cookbook. The flavor:
|
|
208
|
+
|
|
209
|
+
```sql
|
|
210
|
+
SELECT SpanAttributes['http.route.controller'] AS controller,
|
|
211
|
+
count() AS requests,
|
|
212
|
+
round(quantile(0.5)(Duration/1e6)) AS p50_ms,
|
|
213
|
+
round(avg(toFloat64OrZero(SpanAttributes['stats.postgres_query_count']))) AS avg_queries
|
|
214
|
+
FROM otel_traces
|
|
215
|
+
WHERE SpanAttributes['main'] = 'true'
|
|
216
|
+
GROUP BY controller ORDER BY p50_ms DESC LIMIT 15
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## In production
|
|
220
|
+
|
|
221
|
+
Wide Events was extracted from a multi-tenant Rails app subject to healthcare privacy rules, where it runs in staging and production today: one row per request and job execution, exported over OTLP into self-hosted ClickStack. A request that runs a hybrid search and drafts a reply with an LLM still lands as one event, carrying the account, the feature flags evaluated, search quality (semantic hits kept, top cosine similarity), the model, tokens, latency, and cost, the Postgres query count, and an error slug for anything rescued along the way. [docs/production-example.md](docs/production-example.md) shows a full anonymized event, the instrumentation patterns behind it, and two standing queries taken from that deployment.
|
|
110
222
|
|
|
111
223
|
## Conventions
|
|
112
224
|
|
|
113
225
|
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.
|
|
114
226
|
|
|
115
|
-
Unhandled exceptions get `error: true` with no slug, deliberately: the missing slug marks the rescue you haven't instrumented yet.
|
|
116
|
-
|
|
117
227
|
## Development
|
|
118
228
|
|
|
119
229
|
```bash
|
|
@@ -21,6 +21,23 @@ module WideEvents
|
|
|
21
21
|
create_file "AGENTS.md", section
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
def wire_test_helper
|
|
26
|
+
path = File.join(destination_root, "test/test_helper.rb")
|
|
27
|
+
content = File.exist?(path) ? File.read(path) : ""
|
|
28
|
+
if content.match?(%r{require "rails/test_help"\n}) && content.match?(/class TestCase\n/)
|
|
29
|
+
inject_into_file "test/test_helper.rb", "require \"wide_event/test_helper\"\n",
|
|
30
|
+
after: %r{require "rails/test_help"\n}
|
|
31
|
+
inject_into_file "test/test_helper.rb", " include WideEvent::TestHelper\n",
|
|
32
|
+
after: /class TestCase\n/
|
|
33
|
+
else
|
|
34
|
+
say_status :skip, "test/test_helper.rb (not found or unrecognized) - wire the test helper manually:", :yellow
|
|
35
|
+
say <<~MSG
|
|
36
|
+
require "wide_event/test_helper" # after rails/test_help
|
|
37
|
+
include WideEvent::TestHelper # inside ActiveSupport::TestCase
|
|
38
|
+
MSG
|
|
39
|
+
end
|
|
40
|
+
end
|
|
24
41
|
end
|
|
25
42
|
end
|
|
26
43
|
end
|
data/lib/wide_event/railtie.rb
CHANGED
|
@@ -22,7 +22,15 @@ module WideEvent
|
|
|
22
22
|
# After the app's own initializers, so the host's OpenTelemetry::SDK
|
|
23
23
|
# configuration (and any WideEvent.configure block) has already run.
|
|
24
24
|
config.after_initialize do
|
|
25
|
-
|
|
25
|
+
if WideEvent.enabled?
|
|
26
|
+
WideEvent.install!
|
|
27
|
+
elsif Rails.env.development?
|
|
28
|
+
Rails.logger&.info(
|
|
29
|
+
"wide_events: installed but disabled (OTEL_EXPORTER_OTLP_ENDPOINT is not set). " \
|
|
30
|
+
"To see events locally, set `config.enabled = true` and `config.sink = :log` " \
|
|
31
|
+
"in config/initializers/wide_events.rb."
|
|
32
|
+
)
|
|
33
|
+
end
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
rake_tasks do
|
data/lib/wide_event/registry.rb
CHANGED
|
@@ -43,11 +43,18 @@ module WideEvent
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Fails if strict mode saw any attribute that isn't declared in the
|
|
46
|
-
# registry
|
|
47
|
-
#
|
|
46
|
+
# registry since the last check, then resets the tally. The reset is what
|
|
47
|
+
# makes a global teardown hook practical: the test that set the
|
|
48
|
+
# undeclared attribute fails; later tests stay green.
|
|
49
|
+
#
|
|
50
|
+
# class ActiveSupport::TestCase
|
|
51
|
+
# include WideEvent::TestHelper
|
|
52
|
+
# teardown { assert_registered_wide_event_attributes }
|
|
53
|
+
# end
|
|
48
54
|
def assert_registered_wide_event_attributes
|
|
49
55
|
registry = WideEvent.config.registry
|
|
50
56
|
violations = registry ? registry.violations : []
|
|
57
|
+
registry&.clear_violations!
|
|
51
58
|
assert violations.empty?,
|
|
52
59
|
"unregistered wide-event attributes #{violations.sort.inspect} - declare them in #{WideEvent.config.registry_path}"
|
|
53
60
|
end
|
data/lib/wide_event/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wide_events
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adam Miribyan
|
|
@@ -81,6 +81,8 @@ metadata:
|
|
|
81
81
|
homepage_uri: https://github.com/adammiribyan/wide_events
|
|
82
82
|
source_code_uri: https://github.com/adammiribyan/wide_events
|
|
83
83
|
changelog_uri: https://github.com/adammiribyan/wide_events/blob/main/CHANGELOG.md
|
|
84
|
+
bug_tracker_uri: https://github.com/adammiribyan/wide_events/issues
|
|
85
|
+
documentation_uri: https://rubydoc.info/gems/wide_events
|
|
84
86
|
rubygems_mfa_required: 'true'
|
|
85
87
|
rdoc_options: []
|
|
86
88
|
require_paths:
|