Recipes
Real-world patterns that combine the stream API, the SSE bridge, and the filesystem reader (readFsLogs / tailFsLogs).
1. Build a minimal devtool
A live event panel is ~30 lines of EventSource glue + a virtual table. The Nuxt playground page (apps/playground/app/pages/stream.vue) is exactly that — fork it, restyle, ship.
The minimum-viable shape, framework-agnostic:
const events: WideEvent[] = []
const es = new EventSource('/api/_evlog/stream')
es.onmessage = (e) => {
const env = JSON.parse(e.data)
if (env.evlog !== '1') return
if (env.type === 'event' || env.type === 'replay') {
events.unshift(env.data)
if (events.length > 500) events.length = 500
render()
}
}
es.addEventListener('ping', () => {
// heartbeat — connection alive
})
Build whatever UI you want around events. React, Vue, Solid, vanilla. Nothing in evlog is opinionated about your renderer.
2. Quick CLI inspection with curl + jq
When you SSH into a self-hosted box, no UI needed:
curl -N http://localhost:3000/api/_evlog/stream \
| jq -c 'select(.type == "event") | .data'
Filter on the client side as needed:
# Only errors
curl -sN http://localhost:3000/api/_evlog/stream \
| jq -c 'select(.type == "event" and .data.level == "error") | .data'
# Only one service
curl -sN http://localhost:3000/api/_evlog/stream \
| jq -c 'select(.type == "event" and .data.service == "checkout") | .data'
# Slow requests
curl -sN http://localhost:3000/api/_evlog/stream \
| jq -c 'select(.type == "event" and .data.duration > 500) | .data'
-N keeps curl in streaming mode (no buffering). -s is silent.
3. Replay history then go live
History on disk (filesystem drain) + live updates from the SSE bridge = a full picture from any point in time.
import { readFsLogs } from 'evlog/fs'
async function bootstrap(handle: (e: WideEvent) => void) {
// 1. Replay the last hour from `.evlog/logs/`
const since = new Date(Date.now() - 60 * 60 * 1000)
for await (const event of readFsLogs({ since })) {
handle(event)
}
// 2. Switch to the live SSE stream
const es = new EventSource('/api/_evlog/stream')
es.onmessage = (e) => {
const env = JSON.parse(e.data)
if (env.evlog !== '1') return
if (env.type === 'event' || env.type === 'replay') {
handle(env.data)
}
}
return () => es.close()
}
readFsLogs skips files outside the date range, so the replay step is fast even if you keep weeks of history. For a tail-only mode without on-disk replay, point at the SSE endpoint with ?since=<iso> to reuse the in-process ring buffer instead.
4. Filter, transform, aggregate on the consumer
Keep the bridge dumb — every consumer picks what it cares about:
// Just errors
const errors = events.filter(e => e.level === 'error')
// Slow requests
const slowReqs = events.filter(e => typeof e.duration === 'number' && e.duration > 500)
// Group by service
const byService = Object.groupBy(events, e => e.service)
// Rolling error rate (last 100 events)
const last100 = events.slice(0, 100)
const errorRate = last100.filter(e => e.level === 'error').length / last100.length
// Ad-hoc cost analytics — works because evlog/ai writes ai.* fields on every AI call
const totalCost = events
.filter(e => typeof e.ai?.estimatedCost === 'number')
.reduce((sum, e) => sum + (e.ai?.estimatedCost as number), 0)
For complex transforms (rolling windows, percentiles, derived series), use a lib (rxjs, observable, anything async-iterator-friendly) on top of the same EventSource source.
5. Self-hosted "tail -f" replacement
Skip the SSE bridge entirely if the consumer runs on the same machine:
import { tailFsLogs } from 'evlog/fs'
const ac = new AbortController()
process.on('SIGINT', () => ac.abort())
for await (const event of tailFsLogs({ signal: ac.signal })) {
// Process every wide event as it lands on disk
if (event.level === 'error') notifyOps(event)
}
Works without instrumenting the running app — useful for sidecar / observer processes that watch a directory.
What not to do
- Don't run the SSE bridge on Vercel Functions / Cloudflare Workers / Lambda. Each invocation is a separate isolate; subscribers in one isolate never see events emitted by other isolates. Use a real broker (Redis Streams, NATS, Pub/Sub) for cross-instance fan-out.
- Don't put auth-sensitive data in wide events unless your evlog config redacts them. The stream relays exactly what your app emitted — including any unredacted PII.
- Don't filter at the bridge ("only error events please"). The bridge is purpose-built to be transparent. Filter on the consumer side; that way one filter doesn't starve another consumer.