Agent Guide
When to Use What
Section titled “When to Use What”| Scenario | Use | Example |
|---|---|---|
| Wrap an async function with a span | trace(fn) or span('Name', fn) | Handlers, use-case functions, workers |
| Wrap with explicit name/key | trace('checkout', fn) or instrument({ key: 'checkout', fn }) | When name inference isn’t reliable |
| Need span context (set attributes) | Factory: trace((ctx) => async (args) => { ctx.setAttribute(...); ... }) | Attach attributes inside the function |
| One snapshot per request | getRequestLogger(ctx?) + .set() / .info() / .error() + .emitNow() | HTTP request handlers, background jobs |
| Throw an error with why/fix/link | createStructuredError({ message, why?, fix?, link?, status?, cause? }) | API routes, services, validation |
| Show API error in UI (client) | parseError(caught) → use message, why, fix, link | Toasts, error banners, forms |
| Product/analytics events | track('event.name', attributes) or Event from autotel/event | Clicks, signups, conversions |
| Record error on current span | recordStructuredError(ctx, error) or request logger .error() | Inside catch blocks when you have a span |
| Security decision point | securityEvent() from autotel-audit | Login failure, access.tenant.violation |
| Wrap a sensitive operation | withSecurity() from autotel-audit | API key creation |
| Correlate actor without raw PII | hashIdentifier() from autotel-audit | Email or IP in actorId |
| Validation mismatch observability | defineValidator() from autotel/validate | POST body shape at boundary |
| Zero-code probe/401/LLM signals | createSecuritySignalProcessor() in init({ spanProcessors }) | Scanner traffic, credential stuffing |
Rule of thumb: If there’s an HTTP request or a “job”, create a span (via trace() or framework middleware) and use getRequestLogger() when you want one coherent snapshot. Use createStructuredError for any error that should be explainable to users or agents.
Before/After Examples
Section titled “Before/After Examples”1. Uninstrumented handler → trace + request logger
Section titled “1. Uninstrumented handler → trace + request logger”Before:
export async function postCheckout(req: Request, res: Response) { const user = await getAuth(req); const body = await readBody(req); const result = await processCheckout(user.id, body); return res.json(result);}After:
import { trace, getRequestLogger } from 'autotel';
export const postCheckout = trace( (ctx) => async (req: Request, res: Response) => { const log = getRequestLogger(ctx); const user = await getAuth(req); log.set({ user: { id: user.id } });
const body = await readBody(req); log.set({ cart: { items: body.items?.length } });
const result = await processCheckout(user.id, body); log.set({ result: { orderId: result.id } }); log.emitNow(); return res.json(result); },);If the framework already creates a span per request (e.g. Autotel Hono middleware), you can call getRequestLogger() with no args inside the handler instead of passing ctx.
2. Generic error → structured error
Section titled “2. Generic error → structured error”Before:
if (!user) throw new Error('User not found');// orcatch (e) { throw new Error('Payment failed');}After:
import { createStructuredError } from 'autotel';
if (!user) { throw createStructuredError({ message: 'User not found', status: 404, why: `No user with ID "${userId}"`, fix: 'Check the user ID and try again', link: 'https://docs.example.com/errors/user-not-found', });}
try { await processPayment(data);} catch (e) { throw createStructuredError({ message: 'Payment failed', status: 402, why: e instanceof Error ? e.message : 'Unknown error', fix: 'Try a different payment method or contact support', link: 'https://docs.example.com/payments', cause: e, });}3. Client: raw catch → parseError and UI
Section titled “3. Client: raw catch → parseError and UI”Before:
try { await fetch('/api/checkout', { method: 'POST', body: JSON.stringify(data) });} catch (err) { toast.error('Something went wrong');}After:
import { parseError } from 'autotel';
try { await fetch('/api/checkout', { method: 'POST', body: JSON.stringify(data) });} catch (err) { const error = parseError(err); toast.error(error.message, { description: error.why, action: error.fix ? { label: 'Fix', onClick: () => showHelp(error.fix) } : undefined, }); if (error.link) setDocLink(error.link);}4. Scattered console.log → request logger
Section titled “4. Scattered console.log → request logger”Before:
export default defineEventHandler(async (event) => { console.log('Checkout started'); const user = await requireAuth(event); console.log('User:', user.id); const cart = await getCart(user.id); console.log('Cart items:', cart.items.length); const result = await processCheckout(cart); console.log('Order:', result.id); return result;});After:
import { trace, getRequestLogger } from 'autotel';
export default trace((ctx) => async (event) => { const log = getRequestLogger(ctx); const user = await requireAuth(event); log.set({ user: { id: user.id } });
const cart = await getCart(user.id); log.set({ cart: { items: cart.items.length } });
const result = await processCheckout(cart); log.set({ order: { id: result.id } }); log.emitNow(); return result;});(If the framework attaches the event to an existing span, use getRequestLogger() with no args and omit the outer trace if the framework already creates the span.)
5. Failed login without telemetry → securityEvent() hook
Section titled “5. Failed login without telemetry → securityEvent() hook”Before:
export const postLogin = trace((ctx) => async (req, res) => { const { email, password } = req.body; const user = await findUser(email); if (!user || !(await verifyPassword(user, password))) { return res.status(401).json({ error: 'Invalid credentials' }); } return res.json({ token: await issueToken(user) });});After:
import { trace } from 'autotel';import { securityEvent, hashIdentifier } from 'autotel-audit';
export const postLogin = trace((ctx) => async (req, res) => { const { email, password } = req.body; const user = await findUser(email); if (!user || !(await verifyPassword(user, password))) { securityEvent({ name: 'auth.login.failed', category: 'authentication', outcome: 'failure', severity: 'warning', actorId: hashIdentifier(email), reason: 'invalid_password', }); return res.status(401).json({ error: 'Invalid credentials' }); } return res.json({ token: await issueToken(user) });});The handler still enforces auth. Autotel records the signal. See Security Observability for setup and metrics.
Framework Setup Snippets
Section titled “Framework Setup Snippets”import { Hono } from 'hono';import { init, getRequestLogger } from 'autotel';import { autotelMiddleware } from 'autotel-hono';
init({ service: 'my-api' });
const app = new Hono();app.use('*', autotelMiddleware());
app.post('/api/checkout', async (c) => { const log = getRequestLogger(); log.set({ route: 'checkout' }); log.emitNow(); return c.json({ ok: true });});Fastify (conceptual)
Section titled “Fastify (conceptual)”import Fastify from 'fastify';import { init, trace, getRequestLogger } from 'autotel';
init({ service: 'my-api' });
// Register middleware that creates a span per request (see example app).// In route handler:app.post('/api/checkout', async (request, reply) => { return trace((ctx) => async () => { const log = getRequestLogger(ctx); log.set({ route: 'checkout' }); const result = await handleCheckout(request); log.emitNow(); return result; })();});TanStack Start
Section titled “TanStack Start”See packages/autotel-tanstack and apps/example-tanstack-start: middleware and env config. Use getRequestLogger() inside server handlers when a span is active.
Cloudflare Workers
Section titled “Cloudflare Workers”Use autotel-cloudflare. It wraps fetch so each request gets a span and provides full bindings instrumentation (KV, R2, D1, Durable Objects, Workers AI, Vectorize, Queues, etc.). Use getRequestLogger() or trace context inside the handler.
Audit & Security Observability
Section titled “Audit & Security Observability”Use the optional autotel-audit package for compliance and security telemetry:
- Audit trail (
withAudit,forceKeepAuditEvent,setAuditAttributes): who did what to which resource. - Security signals (
securityEvent,withSecurity): abuse and detection events at auth, access control, and secret-handling decision points. - Zero-code HTTP signals: register
createSecuritySignalProcessor()ininit({ spanProcessors })for probe paths, denied responses, and auth bursts.
The autotel-genai/agent entry point adds agent-layer audit (withAgentAction, withAgentToolCall, recordPolicyDecision) for tool calls, policy decisions, and delegation.
See Audit Logging and Security Observability.
Best-effort by default
Section titled “Best-effort by default”Audit and agent telemetry is observability — a missing trace context must never crash the business logic it wraps. withAudit, withAgentAction, withAgentToolCall, recordPolicyDecision, and securityEvent / withSecurity no longer throw into your handler when there is no active trace context. An onMissingContext option (on each wrapper’s options arg) controls the behaviour:
'warn'(default): run the wrapped handler un-audited and log one warning per action.'skip': run un-audited, silently.'throw': previous fail-fast behaviour — opt in when telemetry is mandatory.
This makes the agent layer safe to drop into a production hot path with no surrounding trace() and no try/catch.
Works beyond autotel’s own trace()
Section titled “Works beyond autotel’s own trace()”The agent wrappers resolve trace context from any active OpenTelemetry span, not only inside autotel’s own trace(). They compose inside @effect/opentelemetry, a vanilla OpenTelemetry NodeSDK, and autotel-cloudflare-instrumented fetch handlers and Cloudflare Workflows. For example, inside a step.do(...) of an instrumentWorkflow-wrapped WorkflowEntrypoint:
import { withAgentToolCall } from 'autotel-genai/agent';
await step.do('research', async () => withAgentToolCall( { action: 'agent.research', agent: { id: 'researcher' }, tool: { name: model, input: { prompt } }, }, () => env.AI.run(model, { messages }), ),);Generic Node HTTP
Section titled “Generic Node HTTP”import { init, trace, getRequestLogger } from 'autotel';
init({ service: 'my-api' });
server.on('request', (req, res) => { trace((ctx) => async () => { const log = getRequestLogger(ctx); log.set({ method: req.method, path: req.url }); try { const result = await handleRequest(req, res); log.emitNow(); return result; } catch (e) { log.error(e); log.emitNow(); throw e; } })();});Adding a New Framework Integration (touchpoints)
Section titled “Adding a New Framework Integration (touchpoints)”When adding Autotel support for a new framework (e.g. a new web framework):
-
New package or entry in existing package Create middleware/plugin that: (a) creates a span per request, (b) optionally runs in AsyncLocalStorage so
getRequestLogger()can be called with no args. -
Touchpoints to update
- New source: e.g.
packages/autotel-<name>/src/index.ts(or new package). - Build: add entry in
tsdown.config.ts/ package build. - Exports: add in
package.jsonexports and typesVersions. - Tests: add
*.test.tsfor middleware (span created, request logger available). - Example app: add under
apps/example-<name>and wire init + middleware. - Docs: update
AGENTS.mdframework table and this guide with a short snippet. - Root: add workspace package and any scripts (e.g.
pnpm --filter example-<name> start).
- New source: e.g.
-
Shared behavior Reuse existing patterns: one span per request, safe headers (no secrets), and optional integration with
getRequestLogger()andcreateStructuredErrorin route handlers. -
Do not Use
await import()for init; keep init synchronous. Do not add barrel re-exports that break tree-shaking.
Checklist Summary
Section titled “Checklist Summary”- Handlers wrapped with
trace()or framework middleware that creates a span - Request-scoped context via
getRequestLogger()and.emitNow()where needed - Thrown errors use
createStructuredError({ message, why?, fix?, link?, status?, cause? }) - Client uses
parseError(err)and shows message/why/fix - No raw
console.logfor request/context when request logger is available - No
await import()at init; usenode-requirehelpers if needed - No secrets or full PII in attributes or logs
- Security decision points emit
securityEvent()or usewithSecurity() -
createSecuritySignalProcessor()registered ininit()for zero-code HTTP signals - PII in security events uses
hashIdentifier(), never raw values
Claude Code Skill
Section titled “Claude Code Skill”Autotel ships a Claude Code skill at .claude/skills/autotel/ with detailed reference guides for wide events, structured errors, request loggers, and code review anti-patterns. See the Claude Code Skill page for full details.