Tail Logger
Tail Worker that stores recent traces from another Worker in KV - live log tail and debugging pattern
A Tail Worker that receives TraceItem batches from producer Workers, keeps the last 50 events in Workers KV, and exposes them over HTTP for inspection.
Features
tailhandler - receives TraceItem batches and stores keyrecentGET /logs- read recent eventsDELETE /logs- clear stored events
API Reference
GET /logs
Example Request
curl "https://your-worker.workers.dev/logs"Success Response
{
"events": [
{
"scriptName": "my-producer",
"outcome": "ok",
"eventTimestamp": 1710000000000,
"logs": ["request handled"]
}
],
"count": 1
}DELETE /logs
Example Request
curl -X DELETE "https://your-worker.workers.dev/logs"Success Response
{
"cleared": true
}Configuring a producer
In the producer Worker's wrangler.json, add this Worker as a tail consumer:
{
"name": "my-producer",
"tail_consumers": [{ "service": "tail-logger" }]
}Deploy tail-logger first, then the producer. When the producer handles requests, its traces are delivered to this Worker's tail handler.
Use Cases
- Learn Tail Workers /
tail_consumerswiring - Debug producer Workers without streaming
wrangler taillocally - Keep a short rolling window of outcomes and console logs
- Teach observability patterns across Workers
Limitations
- Stores only the last 50 events in a single KV key
- Tail delivery from another Worker typically requires deployed Workers
- No filtering UI - raw recent events only
- No authentication on
/logs
Use in your project
Copy these files into an existing Worker. Prefer Deployment to try the full experiment first. Source: apps/experiments/tail-logger.
import { MAX_RECENT_EVENTS, RECENT_KEY } from "../constants/defaults";import type { TailLogEntry } from "../types/log";export function extractLogMessages(event: TraceItem): string[] { if (!event.logs || !Array.isArray(event.logs)) return []; return event.logs.map((log) => { if (typeof log.message === "string") return log.message; if (Array.isArray(log.message)) { return log.message.map((part) => String(part)).join(" "); } return String(log.message ?? ""); });}export function toTailLogEntry(event: TraceItem): TailLogEntry { return { scriptName: event.scriptName ?? "unknown", outcome: event.outcome, eventTimestamp: event.eventTimestamp ?? Date.now(), logs: extractLogMessages(event), };}export async function readRecent(kv: KVNamespace): Promise<TailLogEntry[]> { const raw = await kv.get(RECENT_KEY); if (!raw) return []; try { const parsed = JSON.parse(raw) as unknown; if (!Array.isArray(parsed)) return []; return parsed as TailLogEntry[]; } catch { return []; }}export async function appendTailEvents( kv: KVNamespace, events: TraceItem[]): Promise<TailLogEntry[]> { const existing = await readRecent(kv); const incoming = events.map(toTailLogEntry); const merged = [...incoming, ...existing].slice(0, MAX_RECENT_EVENTS); await kv.put(RECENT_KEY, JSON.stringify(merged)); return merged;}export async function clearRecent(kv: KVNamespace): Promise<void> { await kv.delete(RECENT_KEY);}Deployment
Configure KV and producers
Create a KV namespace and bind it as TAIL_LOGS. Deploy this Worker, then add "tail_consumers": [{ "service": "tail-logger" }] to producer Workers.
Local Development
cd apps/experiments/tail-logger
npm install
npm run devcurl "http://localhost:8787/logs"Tail delivery from another Worker typically requires deployed Workers; unit tests call the tail export directly with mock events.
Configuration
wrangler.json declares:
- KV namespace
TAIL_LOGS
Cloudflare Features Used
- Workers - Edge compute runtime
- Tail Workers - Consume traces from other Workers
- Workers KV - Rolling store for recent events