Skip to content

Commit 62a224f

Browse files
miknampagent
andcommitted
docs: rewrite README around why wide events exist
Amp-Thread-ID: https://ampcode.com/threads/T-019cbfd1-7370-72af-99b9-7b28b315f8cf Co-authored-by: Amp <amp@ampcode.com>
1 parent 8551849 commit 62a224f

1 file changed

Lines changed: 89 additions & 50 deletions

File tree

README.md

Lines changed: 89 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,99 +4,138 @@
44
[![docs.rs](https://docs.rs/wide-event/badge.svg)](https://docs.rs/wide-event)
55
[![License](https://img.shields.io/crates/l/wide-event.svg)](LICENSE-MIT)
66

7-
Honeycomb-style wide events for Rust.
7+
[Wide events](https://charity.wtf/2022/08/15/live-your-best-life-with-structured-events/)
8+
for Rust, built on `tracing`. One structured event per request with all
9+
fields accumulated over the request lifecycle, emitted as a single JSON
10+
line (or logfmt) on completion.
811

9-
A wide event accumulates key-value pairs throughout a request (or task)
10-
lifecycle and emits them as a **single structured event** when the request
11-
completes. This gives you one row per request in your log aggregator with
12-
every dimension attached — perfect for high-cardinality exploratory analysis.
12+
## Why
1313

14-
## Quick start
14+
A span tree for one request can be 10–30 spans. A wide event is one row
15+
with all the dimensions attached. Cheaper to store and query than a span
16+
tree, and retains per-request detail that pre-aggregated metrics lose.
17+
18+
### Metrics from events
19+
20+
An emit hook receives the accumulated fields before serialization. You
21+
can update Prometheus counters and histograms from the event data
22+
directly, which keeps metric definitions next to the event they describe
23+
rather than scattered as `counter.inc()` calls across the codebase.
24+
25+
This means you don't end up with a counter that's incremented on a code
26+
path that doesn't emit enough context to debug it, or an event that's
27+
missing the metric update. The event is the source of truth; metrics are
28+
a view of it.
29+
30+
### Dynamic fields
31+
32+
With tracing spans, fields must be declared at span creation as `Empty`
33+
slots, and each `Span::record` call dispatches through the full
34+
subscriber stack. Wide event setters are `HashMap` inserts behind a local
35+
`Mutex` — any key, any time, no subscriber dispatch until emit.
36+
37+
This matters when telemetry is contributed by several layers that don't
38+
know about each other's fields. Middleware sets `auth.user_id`, the
39+
handler sets `project_id`, the proxy layer sets `upstream_latency_ns`,
40+
error recovery sets `error.type`. Each just calls `event.set_str(...)` on
41+
the shared `WideEvent` without needing a reference to a root span or
42+
knowing what other fields exist.
43+
44+
### Complements tracing
45+
46+
Wide events and tracing spans answer different questions. Spans give you
47+
the call tree — where time was spent within a request. Wide events give
48+
you the outcome — one flat row you can filter and aggregate across
49+
requests.
50+
51+
With the `opentelemetry` feature enabled, `trace_id` and `span_id` from
52+
the current span context are attached to the event automatically. So you
53+
can query wide events to find interesting requests, then jump to the
54+
trace for the ones that need deeper investigation.
55+
56+
## Usage
1557

1658
```rust
1759
use wide_event::{WideEventGuard, WideEventLayer};
1860
use tracing_subscriber::prelude::*;
1961

20-
// Once at startup:
2162
tracing_subscriber::registry()
2263
.with(WideEventLayer::stdout().with_system("myapp"))
2364
.init();
2465

25-
// Per request — guard auto-emits on drop:
2666
{
2767
let req = WideEventGuard::new("http");
2868
req.set_str("method", "GET");
2969
req.set_str("path", "/api/users");
3070
req.set_u64("status", 200);
31-
} // emitted here as single JSON line
71+
} // emitted here
3272
```
3373

34-
## How it works
74+
`WideEventGuard` emits on drop. Use `WideEvent` directly if you want
75+
explicit `emit()` control.
76+
77+
For async task propagation, enable the `tokio` feature and use
78+
`context::scope` / `context::current`.
3579

36-
1. `WideEvent::new` starts a timer and creates an empty field map.
37-
2. Throughout processing, call setters (`set_str`, `set_u64`, `incr`, etc.)
38-
to accumulate fields — these are cheap local `Mutex` operations that never
39-
touch the tracing subscriber.
40-
3. `WideEvent::emit` (or `WideEventGuard` drop) finalizes the record, pushes
41-
it to a thread-local stack, and dispatches a structured `tracing::info!`
42-
event. The `WideEventLayer` pulls the record from the stack, formats the
43-
timestamp, and serializes in a single pass.
80+
## Internals
81+
82+
1. `WideEvent::new` — starts a timer, allocates an empty field map.
83+
2. Setters (`set_str`, `set_u64`, `set_bool`, `incr`, …) — `Mutex`
84+
lock + `HashMap::insert`. No subscriber dispatch.
85+
3. `emit()` — finalizes the record, calls the emit hook if set, pushes
86+
to a thread-local stack, fires `tracing::info!`. The `WideEventLayer`
87+
pops the record and serializes it in a single pass.
88+
89+
Field keys are `&'static str` so setter calls don't allocate for the key.
90+
Field names are almost always string literals, so this is a natural fit.
91+
92+
The thread-local emit stack avoids cross-thread synchronization between
93+
`emit()` and the layer. The layer pulls the record from the stack during
94+
the `tracing::info!` dispatch on the same thread.
4495

4596
## Features
4697

47-
| Feature | Description |
98+
| Feature | What it does |
4899
|---------|-------------|
49-
| `opentelemetry` | Attaches `trace_id` and `span_id` from the current OpenTelemetry span context |
50-
| `tokio` | Provides `context::scope` and `context::current` for async task-local wide event propagation |
100+
| `opentelemetry` | Attaches `trace_id` / `span_id` from current OTel context |
101+
| `tokio` | Task-local propagation via `context::scope` / `context::current` |
51102

52103
```toml
53104
[dependencies]
54105
wide-event = { version = "0.1", features = ["tokio"] }
55106
```
56107

57-
## Formatter options
108+
## Formatters
58109

59-
- **`JsonFormatter`** (default) — one JSON object per line
60-
- **`LogfmtFormatter`**`key=value` pairs per line
110+
`JsonFormatter` (default) or `LogfmtFormatter`:
61111

62112
```rust
63113
use wide_event::{WideEventLayer, LogfmtFormatter};
64-
65114
let layer = WideEventLayer::new(std::io::stdout(), LogfmtFormatter);
66115
```
67116

68-
## Performance
117+
Custom timestamp formatting via `with_timer()` — accepts any
118+
`tracing_subscriber::fmt::time::FormatTime` implementation:
69119

70-
- Field keys are `&'static str` — zero allocation on every setter call. Field
71-
names are almost always string literals, so this is natural and avoids the
72-
`key.to_string()` overhead entirely.
73-
- Field setters (`set_str`, `set_u64`, `incr`, …) are cheap local `Mutex`
74-
operations — they never interact with the tracing subscriber.
75-
- Timestamp formatting reuses a thread-local buffer — no `String` allocation
76-
per emit.
77-
- Serialization happens once at emit time in a single pass over the accumulated
78-
fields.
79-
- A thread-local emit stack avoids cross-thread synchronization on the hot path.
120+
```rust
121+
use wide_event::WideEventLayer;
122+
use tracing_subscriber::fmt::time::Uptime;
123+
124+
let layer = WideEventLayer::stdout().with_timer(Uptime::default());
125+
```
126+
127+
The default timer is `Rfc3339` — microsecond-precision RFC 3339
128+
timestamps using `humantime`.
80129

81130
## Development
82131

83132
```bash
84-
# Set up pre-push hook (runs fmt, clippy, tests before each push)
85-
git config core.hooksPath .githooks
86-
87-
# Release a new version (bumps Cargo.toml, commits, tags, pushes)
88-
# CI publishes to crates.io automatically on tag push.
89-
cargo release patch # or: minor, major
133+
git config core.hooksPath .githooks # pre-push: fmt + clippy + test
134+
cargo release patch # or: minor, major
90135
```
91136

92-
Requires [`cargo-release`](https://crates.io/crates/cargo-release):
93-
`cargo install cargo-release`
137+
Requires [`cargo-release`](https://crates.io/crates/cargo-release).
94138

95139
## License
96140

97-
Licensed under either of
98-
99-
- [MIT license](LICENSE-MIT)
100-
- [Apache License, Version 2.0](LICENSE-APACHE)
101-
102-
at your option.
141+
MIT or Apache-2.0, at your option.

0 commit comments

Comments
 (0)