| name | autotel |
| description | Use when instrumenting with trace/span/track, reviewing code for logging and observability patterns, converting console.log to wide events, adding structured errors, setting up canonical log lines, configuring init(), adding subscribers, or working in the autotel monorepo. |
Autotel
Philosophy: "Write once, observe everywhere" - instrument once, stream to any OTLP-compatible backend.
trace() wraps functions. getRequestLogger() accumulates context. createStructuredError() adds why/fix/link to errors. Canonical log lines emit one wide event per request automatically.
When to Use
- Instrumenting functions with tracing
- Code uses
console.log / console.error for observability
- Error handling lacks structured context (no why, fix, or link)
- Adding tracing to any Node.js or edge runtime handler
- Reviewing code for observability anti-patterns
- Setting up observability in Cloudflare Workers, Hono, or Next.js
- Working in the autotel monorepo
Quick Reference
Tracing API
import { trace, span } from 'autotel';
export const createUser = trace((ctx) => async (data) => {
ctx.setAttribute('user.id', data.id);
return await db.users.create(data);
});
export const getUser = trace(async (id) => {
return await db.users.findById(id);
});
span('db.insert', async () => {
await db.insert(record);
});
Recording Errors
Default: throw, don't catch. trace() records status, exception, and structured attributes when the wrapped function rejects.
import { trace, createStructuredError } from 'autotel';
export const charge = trace((ctx) => async (cart) => {
if (!cart.items.length) {
throw createStructuredError({
message: 'Cart is empty',
why: 'User submitted checkout with no items',
fix: 'Validate cart on the client before submit',
link: 'https://docs.example.com/errors/empty-cart',
});
}
return await processCart(cart);
});
Fallbacks, in order:
- Attach call-site context, then rethrow —
getRequestLogger(ctx).error(err, { step }). Use when the rethrown error needs context only known at the catch site.
- Writing instrumentation/middleware that wraps user handlers —
ctx.recordError(err) from inside a trace((ctx) => ...) callback. Sets ERROR status, structured error.* attributes, and (during the back-compat window) records the exception. Accepts unknown so no as Error cast is needed in catch blocks. For code that doesn't have a ctx handle, use the standalone form recordStructuredError(ctx, err).
return trace({ name }, async (ctx) => {
try {
return await userHandler(args);
} catch (err) {
ctx.recordError(err);
throw err;
}
});
ctx.recordException(...) and ctx.addEvent(...) are intentionally hidden from the TraceContext type per OTEP 4430 (March 2026 — Span Event API deprecation). The runtime methods exist for back-compat only; new code MUST go through createStructuredError, ctx.recordError(err) / recordStructuredError(ctx, err), or the request logger.
Request Logger
import { trace, getRequestLogger } from 'autotel';
export const handleOrder = trace((ctx) => async (req) => {
const log = getRequestLogger(ctx);
log.set({ feature: 'checkout', tier: req.user.tier });
const cart = await loadCart(req.cartId);
log.set({ cart_items: cart.items.length, cart_total: cart.total });
try {
const payment = await processPayment(cart);
log.set({ payment_method: payment.method });
} catch (error) {
log.error(error, { step: 'payment' });
throw error;
}
});
Event Tracking
import { trace, getEventQueue } from 'autotel';
export const signup = trace((ctx) => async (data) => {
ctx.track('user.signup', { userId: data.id, plan: data.plan });
return await db.users.create(data);
});
import { track } from 'autotel';
track('user.signup', { userId: '123', plan: 'pro' });
await getEventQueue()?.flush();
Correlation ID
import { getOrCreateCorrelationId, runWithCorrelationId } from 'autotel';
const correlationId = getOrCreateCorrelationId();
runWithCorrelationId(incomingId, () => handleRequest());
Framework Adapters
import { init, wrapModule, trace } from 'autotel/workers';
const processOrder = trace(async (orderId: string, kv: KVNamespace) => {
return await kv.get(orderId);
});
export default wrapModule(
{ service: { name: 'my-worker' } },
{
async fetch(_req, env) {
return Response.json(await processOrder('123', env.ORDERS_KV));
},
},
);
import { withAutotel, useLogger } from 'autotel-adapters/next';
export const POST = withAutotel(async (request) => {
const log = useLogger(request);
log.set({ feature: 'checkout' });
return Response.json({ ok: true });
});
import { useLogger } from 'autotel-adapters/hono';
app.get('/orders/:id', (c) => {
const log = useLogger(c);
log.set({ route: c.req.path });
return c.json({ ok: true });
});
Anti-Patterns to Detect
| Anti-Pattern | Fix |
|---|
console.log('user created', userId) | log.set({ user_id: userId }) inside trace() |
catch (e) { throw e } | Delete the catch — trace() records errors automatically. Or log.error(e, { step }); throw e to attach call-site context |
catch (e) { res.json({ error: e.message }) } | parseError(e) for consistent shape |
throw new Error('Payment failed') | createStructuredError({ message, why, fix, link }) |
ctx.recordException(err) / span.recordException(err) | App code: throw createStructuredError(...). Instrumentation: ctx.recordError(err) (or recordStructuredError(ctx, err) if you don't have a ctx handle). Span Event API is deprecated (OTEP 4430) and type-gated out of TraceContext |
ctx.addEvent('name', { ... }) / span.addEvent(...) | Discrete event inside trace(): ctx.track('event.name', { ... }) (or standalone track('event.name', { ... }) when there's no ctx handle). Wide-event attribute: getRequestLogger(ctx).set({ ... }) |
(ctx as any).recordException(err) / as unknown as { recordException } | Don't bypass the type gate — use recordStructuredError(ctx, err) instead |
Manual console.log at start/end of function | trace() wrapper handles lifecycle |
| Separate request ID generation | ctx.correlationId provides automatic correlation |
init() Configuration
Signals: Traces, Metrics, Logs
When endpoint is set, traces and metrics are auto-configured by default. Logs are opt-in to avoid unexpected export and preserve OTel SDK OTEL_LOGS_EXPORTER handling:
init({
service: 'my-app',
endpoint: 'http://localhost:4318',
logs: true,
});
Disable any signal explicitly:
init({
service: 'my-app',
endpoint: 'http://localhost:4318',
metrics: false,
logs: false,
});
Custom logRecordProcessors are additive — they work alongside the auto-configured exporter:
init({
service: 'my-app',
endpoint: 'http://localhost:4318',
logRecordProcessors: [customProcessor],
});
Protocol selection (http default, grpc optional) applies to all signals. gRPC exporters are optional peer deps.
Logger Locking
Framework plugins can lock init() to prevent re-initialization:
import { lockLogger, isLoggerLocked } from 'autotel';
lockLogger();
isLoggerLocked();
Silent Mode
Suppress internal autotel logs while keeping exporters running:
init({
service: 'my-app',
silent: true,
minLevel: 'warn',
});
Attribute Redaction
Automatically redact PII from span attributes:
init({
service: 'my-app',
attributeRedactor: 'default',
});
default — Emails, phones, SSNs, credit cards (last 4), sensitive keys
strict — Plus JWTs, Bearer tokens, IBANs, API keys
pci-dss — Focused on payment card data
Custom:
init({
attributeRedactor: {
keyPatterns: [/password/i, /secret/i],
valuePatterns: [{ name: 'customerId', pattern: /CUST-\d{8}/g, replacement: 'CUST-***' }],
builtins: ['email', 'creditCard'],
},
});
Sampling
Default: AdaptiveSampler with 10% baseline, 100% for errors and slow requests (>1s). Tail sampling via TailSamplingSpanProcessor defers the decision until span ends.
import { AdaptiveSampler } from 'autotel';
init({
service: 'my-app',
endpoint: 'http://localhost:4318',
sampler: new AdaptiveSampler({
baselineSampleRate: 0.1,
slowThresholdMs: 1000,
alwaysSampleErrors: true,
alwaysSampleSlow: true,
linksBased: false,
}),
});
The Sampler interface is simple — return true to trace, false to skip:
const sampler: Sampler = {
shouldSample({ operationName, args, links }) {
return operationName.startsWith('critical.');
},
};
Canonical Log Lines
init({
service: 'my-app',
canonicalLogLines: {
enabled: true,
rootSpansOnly: true,
keep: [{ status: 500 }, { durationMs: 1000 }],
pretty: true,
drain: (event) => sendToAnalytics(event),
},
});
MUST / SHOULD / NEVER
Init & Module Loading
- MUST: Keep
init() synchronous
- MUST: Use
safeRequire/requireModule from ./node-require for dynamic deps
- NEVER: Use
await import() for optional/lazy dependencies
Tracing
- MUST: Use
trace(), span(), instrument() to wrap business logic
- MUST: Use factory pattern
trace((ctx) => ...) when setting attributes
- SHOULD: Let trace names infer from const/function names
- NEVER: Manually start/end spans for app logic (SDK glue only)
Errors & Events
- MUST: Throw
createStructuredError({ message, why, fix, link }) instead of new Error(...) in app code — let trace() record it on span exit
- MUST: Use
ctx.recordError(err) from instrumentation/middleware code that wraps user handlers (or recordStructuredError(ctx, err) if you don't have a ctx handle)
- SHOULD: Only catch errors when you need to attach call-site context, then
getRequestLogger(ctx).error(err, { step }) and rethrow
- SHOULD: Emit discrete events inside
trace() with ctx.track('event.name', { ... }) (or standalone track('event.name', { ... }) outside trace()); emit wide-event attributes with getRequestLogger(ctx).set({ ... })
- NEVER: Call
ctx.recordException(err) or ctx.addEvent(...) — Span Event API is deprecated (OTEP 4430, March 2026) and intentionally type-gated out of TraceContext
- NEVER: Cast
ctx as any or as unknown as { recordException } to bypass the type gate
Event Queue
- MUST: Call
getEventQueue()?.flush() before assertions or shutdown
- MUST: Forward
options.autotel in subscriber payloads (contains trace context)
- NEVER: Assert on event delivery without flush
Tree-Shaking & Repository
- MUST: Use explicit
exports in package.json for new entry points
- MUST: Ask before adding new dependencies or modifying build configs
- MUST: Create changeset for any package changes (
pnpm changeset)
- NEVER: Add barrel re-exports that pull in unused code
Package Layout
| Package | Role |
|---|
autotel | Node.js core: init, trace, span, track, event-queue, correlation-id. Also provides autotel/workers and autotel/cloudflare for Cloudflare Workers |
autotel-genai | GenAI/LLM instrumentation: traceGenAI, cost, metrics, events, semconv, ai-sdk bridge, and agent governance (autotel-genai/agent) |
autotel-edge | Edge runtime foundation (alternative to workers for vendor-agnostic edge) |
autotel-cloudflare | Cloudflare Workers implementation (re-exported via autotel/workers) |
autotel-adapters | Framework adapters (Next.js, Hono, Nitro) |
autotel-mcp-instrumentation | MCP instrumentation |
autotel-tanstack | TanStack Start |
autotel-subscribers | Event subscribers (PostHog, Mixpanel, Webhook) |
Each package has a CLAUDE.md for local conventions.
Semantic Helpers
import { traceDB, traceHTTP, traceMessaging } from 'autotel/semantic-helpers';
import { traceGenAI, recordGenAiUsage } from 'autotel-genai/trace';
export const generateText = traceGenAI({
model: 'gpt-4-turbo', operation: 'chat', provider: 'openai',
})((ctx) => async (prompt) => {
const response = await openai.chat.completions.create({ });
recordGenAiUsage(ctx, 'gpt-4-turbo', {
inputTokens: response.usage.prompt_tokens,
outputTokens: response.usage.completion_tokens,
});
});
export const getUser = traceDB({
system: 'postgresql', operation: 'SELECT', collection: 'users',
})((ctx) => async (userId) => { });
Type-Safe Attributes
import { attrs, setUser, safeSetAttributes } from 'autotel/attributes';
ctx.setAttributes(attrs.user.id('user-123'));
setUser(ctx, { id: '123', email: 'user@example.com' });
safeSetAttributes(ctx, attrs.user.data({ email: 'pii@example.com' }), {
guardrails: { pii: 'hash' },
});
Producer/Consumer Pattern
import { traceProducer, traceConsumer } from 'autotel/messaging';
export const publish = traceProducer({
system: 'kafka', destination: 'user-events',
messageIdFrom: (args) => args[0].id,
})((ctx) => async (event) => {
const headers = ctx.getTraceHeaders();
await producer.send({ messages: [{ value: event, headers }] });
});
Quick Commands
pnpm build
pnpm test
pnpm lint
pnpm quality
pnpm changeset
Testing
- Unit tests:
*.test.ts / Integration tests: *.integration.test.ts
- MUST flush before assertions:
await getEventQueue()?.flush();
- Use
SubscriberTestHarness for subscriber tests
Loading Reference Files
Advanced Features
See docs/ADVANCED.md for deterministic trace IDs, metadata flattening, isolated tracer providers, safe baggage propagation, and workflow/saga tracing.