Skip to content

Commit ddf0be4

Browse files
miknampagent
andcommitted
feat: initial release — Honeycomb-style wide events for Rust
Amp-Thread-ID: https://ampcode.com/threads/T-019cbd63-d8c4-74a9-914a-549909f4e122 Co-authored-by: Amp <amp@ampcode.com>
0 parents  commit ddf0be4

14 files changed

Lines changed: 1589 additions & 0 deletions

File tree

.githooks/pre-push

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
echo "pre-push: running checks..."
5+
cargo fmt --check
6+
cargo clippy --all-targets --all-features -- -D warnings
7+
cargo test --all-features
8+
RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps
9+
echo "pre-push: all checks passed."

.github/dependabot.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: cargo
4+
directory: /
5+
schedule:
6+
interval: weekly
7+
- package-ecosystem: github-actions
8+
directory: /
9+
schedule:
10+
interval: weekly

.github/workflows/ci.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
env:
9+
CARGO_TERM_COLOR: always
10+
11+
jobs:
12+
test:
13+
name: Test (${{ matrix.toolchain }}, ${{ matrix.features }})
14+
runs-on: ubuntu-latest
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
toolchain: [stable, nightly]
19+
features:
20+
- ""
21+
- --features tokio
22+
- --features opentelemetry
23+
- --all-features
24+
steps:
25+
- uses: actions/checkout@v4
26+
- uses: dtolnay/rust-toolchain@master
27+
with:
28+
toolchain: ${{ matrix.toolchain }}
29+
- run: cargo test ${{ matrix.features }}
30+
31+
clippy:
32+
name: Clippy
33+
runs-on: ubuntu-latest
34+
steps:
35+
- uses: actions/checkout@v4
36+
- uses: dtolnay/rust-toolchain@stable
37+
with:
38+
components: clippy
39+
- run: cargo clippy --all-targets --all-features -- -D warnings
40+
41+
fmt:
42+
name: Format
43+
runs-on: ubuntu-latest
44+
steps:
45+
- uses: actions/checkout@v4
46+
- uses: dtolnay/rust-toolchain@stable
47+
with:
48+
components: rustfmt
49+
- run: cargo fmt --check
50+
51+
doc:
52+
name: Documentation
53+
runs-on: ubuntu-latest
54+
steps:
55+
- uses: actions/checkout@v4
56+
- uses: dtolnay/rust-toolchain@stable
57+
- run: cargo doc --all-features --no-deps
58+
env:
59+
RUSTDOCFLAGS: -D warnings

.github/workflows/release.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
env:
9+
CARGO_TERM_COLOR: always
10+
11+
jobs:
12+
publish:
13+
name: Publish to crates.io
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: dtolnay/rust-toolchain@stable
18+
- run: cargo publish
19+
env:
20+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/target
2+
Cargo.lock

Cargo.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[package]
2+
name = "wide-event"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "Honeycomb-style wide events — accumulate structured fields throughout a request lifecycle and emit as a single JSON line via tracing"
6+
license = "MIT"
7+
repository = "https://github.com/lovablelabs/wide-event"
8+
keywords = ["tracing", "observability", "wide-event", "structured-logging", "honeycomb"]
9+
categories = ["development-tools::debugging", "development-tools::profiling"]
10+
11+
[features]
12+
default = []
13+
opentelemetry = ["dep:opentelemetry", "dep:tracing-opentelemetry"]
14+
tokio = ["dep:tokio"]
15+
16+
[dependencies]
17+
serde = { version = "1.0", features = ["derive"] }
18+
serde_json = "1.0"
19+
tracing = "0.1"
20+
tracing-subscriber = { version = "0.3", default-features = false, features = ["registry", "fmt"] }
21+
22+
opentelemetry = { version = "0.31", default-features = false, features = ["trace"], optional = true }
23+
tracing-opentelemetry = { version = "0.32", optional = true }
24+
tokio = { version = "1", features = ["rt"], optional = true }
25+
26+
[dev-dependencies]
27+
criterion = { version = "0.5", features = ["html_reports"] }
28+
tracing-subscriber = { version = "0.3", features = ["registry", "fmt", "env-filter"] }
29+
tokio = { version = "1", features = ["rt", "macros"] }
30+
31+
[[bench]]
32+
name = "wide_event"
33+
harness = false

LICENSE-MIT

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Lovable
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# wide-event
2+
3+
[![Crates.io](https://img.shields.io/crates/v/wide-event.svg)](https://crates.io/crates/wide-event)
4+
[![docs.rs](https://docs.rs/wide-event/badge.svg)](https://docs.rs/wide-event)
5+
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE-MIT)
6+
7+
Honeycomb-style wide events for Rust.
8+
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.
13+
14+
## Quick start
15+
16+
```rust
17+
use wide_event::{WideEventGuard, WideEventLayer};
18+
use tracing_subscriber::prelude::*;
19+
20+
// Once at startup:
21+
tracing_subscriber::registry()
22+
.with(WideEventLayer::stdout().with_system("myapp"))
23+
.init();
24+
25+
// Per request — guard auto-emits on drop:
26+
{
27+
let req = WideEventGuard::new("http");
28+
req.set_str("method", "GET");
29+
req.set_str("path", "/api/users");
30+
req.set_u64("status", 200);
31+
} // ← emitted here as single JSON line
32+
```
33+
34+
## How it works
35+
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.
44+
45+
## Features
46+
47+
| Feature | Description |
48+
|---------|-------------|
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 |
51+
52+
```toml
53+
[dependencies]
54+
wide-event = { version = "0.1", features = ["tokio"] }
55+
```
56+
57+
## Formatter options
58+
59+
- **`JsonFormatter`** (default) — one JSON object per line
60+
- **`LogfmtFormatter`**`key=value` pairs per line
61+
62+
```rust
63+
use wide_event::{WideEventLayer, LogfmtFormatter};
64+
65+
let layer = WideEventLayer::new(std::io::stdout(), LogfmtFormatter);
66+
```
67+
68+
## Performance
69+
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.
80+
81+
## Development
82+
83+
```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
90+
```
91+
92+
Requires [`cargo-release`](https://crates.io/crates/cargo-release):
93+
`cargo install cargo-release`
94+
95+
## License
96+
97+
MIT — see [LICENSE-MIT](LICENSE-MIT).

0 commit comments

Comments
 (0)