Skip to content

Advanced Features

Generate consistent trace IDs from seeds for correlation with external systems:

import { createDeterministicTraceId } from 'autotel/trace-helpers';
const requestId = req.headers['x-request-id'];
const traceId = await createDeterministicTraceId(requestId);
console.log(`View traces: https://your-backend.com/traces/${traceId}`);

Implementation: Uses SHA-256 hashing to generate consistent 128-bit trace IDs. Works in Node.js and edge runtimes (via crypto.subtle).

Use cases:

  • Correlate external request IDs with OTel traces
  • Link support tickets to trace data
  • Associate business entities (orders, sessions) with observability data

Automatically flatten nested objects into dot-notation span attributes:

import { flattenMetadata } from 'autotel/trace-helpers';
import { trace } from 'autotel';
export const processOrder = trace((ctx) => async (order: Order) => {
const metadata = flattenMetadata({
user: { id: order.userId, tier: 'premium' },
payment: { method: 'card', processor: 'stripe' },
items: order.items.length,
});
ctx.setAttributes(metadata);
// Results in: metadata.user.id, metadata.user.tier, metadata.payment.method, etc.
});

Features:

  • Auto-serializes non-string values to JSON
  • Filters out null/undefined values
  • Gracefully handles circular references (→ <serialization-failed>)
  • Customizable prefix (default: 'metadata')

For library authors who want to use Autotel without interfering with the application’s global OTel setup:

import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { setAutotelTracerProvider } from 'autotel/tracer-provider';
// Create isolated provider (don't call provider.register())
const exporter = new OTLPTraceExporter({
url: 'https://your-backend.com/v1/traces',
});
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
// Set as Autotel's provider (isolated from global OTel)
setAutotelTracerProvider(provider);
// Now all trace(), span(), instrument() calls use this provider

Important limitations:

  • Isolates span processing and export only
  • OpenTelemetry context (trace IDs, parent spans) is still shared globally
  • Spans from isolated provider may inherit context from global spans

Use cases:

  • Library code with embedded Autotel
  • SDKs that need observability without forcing users to configure OTel
  • Separate span processing for different subsystems
  • Testing with isolated trace collection

Pre-configured trace helpers following OpenTelemetry semantic conventions:

import { traceDB, traceHTTP, traceMessaging } from 'autotel/semantic-helpers';
import { traceGenAI, recordGenAiUsage } from 'autotel-genai/trace';
// LLM operations (Gen AI semantic conventions) — live in autotel-genai
export const generateText = traceGenAI({
model: 'gpt-4-turbo',
operation: 'chat',
provider: 'openai',
})((ctx) => async (prompt: string) => {
const response = await openai.chat.completions.create({
/* ... */
});
// Records gen_ai.usage.input_tokens / gen_ai.usage.output_tokens
recordGenAiUsage(ctx, 'gpt-4-turbo', {
inputTokens: response.usage.prompt_tokens,
outputTokens: response.usage.completion_tokens,
});
return response.choices[0].message.content;
});
// Database operations (DB semantic conventions)
export const getUser = traceDB({
system: 'postgresql',
operation: 'SELECT',
database: 'app_db',
collection: 'users',
})((ctx) => async (userId: string) => {
const query = 'SELECT * FROM users WHERE id = $1';
ctx.setAttribute('db.statement', query);
return await pool.query(query, [userId]);
});
// HTTP client operations (HTTP semantic conventions)
export const fetchUser = traceHTTP({
method: 'GET',
url: 'https://api.example.com/users/:id',
})((ctx) => async (userId: string) => {
const response = await fetch(`https://api.example.com/users/${userId}`);
ctx.setAttribute('http.response.status_code', response.status);
return response.json();
});
// Messaging operations (Messaging semantic conventions)
export const publishEvent = traceMessaging({
system: 'kafka',
operation: 'publish',
destination: 'user-events',
})((ctx) => async (event: Event) => {
await producer.send({ topic: 'user-events', messages: [event] });
ctx.setAttribute('messaging.message.id', event.id);
});

Benefits:

  • Automatic semantic attributes following OTel specs
  • Type-safe configuration interfaces
  • Reduces boilerplate by 60-70%
  • Links to official OTel semantic convention docs in JSDoc

Available helpers:

  • traceGenAI() (from autotel-genai/trace) — Gen AI operations (chat, completion, embedding)
  • traceDB() — Database operations (SQL, NoSQL, Redis)
  • traceHTTP() — HTTP client requests
  • traceMessaging() — Queue/messaging operations (Kafka, RabbitMQ, SQS)

First-class support for message-based systems with traceProducer and traceConsumer helpers:

import { traceProducer, traceConsumer } from 'autotel/messaging';
// Producer - auto-sets SpanKind.PRODUCER and semantic attributes
export const publishEvent = traceProducer({
system: 'kafka', // kafka | sqs | rabbitmq | custom
destination: 'user-events',
messageIdFrom: (args) => args[0].id, // Extract message ID
})((ctx) => async (event: Event) => {
const headers = ctx.getTraceHeaders(); // W3C traceparent/tracestate
await producer.send({
topic: 'user-events',
messages: [{ value: event, headers }],
});
});
// Consumer - auto-sets SpanKind.CONSUMER, extracts links from headers
export const processEvent = traceConsumer({
system: 'kafka',
destination: 'user-events',
consumerGroup: 'event-processor',
headersFrom: (msg) => msg.headers, // Extract trace headers
batchMode: true, // For batch consumers
})((ctx) => async (messages) => {
// Links to producer spans automatically created
for (const msg of messages) await process(msg);
});

For KafkaJS eachBatch, use withBatchConsumer from autotel-plugins/kafka for batch-level and optional per-message spans with trace continuation from headers.

Key implementation details:

  • Uses SpanKind.PRODUCER / SpanKind.CONSUMER for proper trace visualization
  • ctx.getTraceHeaders() returns { traceparent, tracestate? } for header injection
  • ctx.recordDLQ(dlqName, reason) for dead-letter queue tracking
  • Supports lag metrics via lagMetrics.getCurrentOffset / getEndOffset
  • Automatic semantic attributes: messaging.system, messaging.destination.name, messaging.operation, messaging.consumer.group

Type-safe baggage schemas with built-in guardrails:

import {
createSafeBaggageSchema,
BusinessBaggage,
} from 'autotel/business-baggage';
// Pre-built schema for common fields
BusinessBaggage.set(ctx, {
tenantId: 'acme',
userId: 'user-123',
priority: 'high',
});
const { tenantId, priority } = BusinessBaggage.get(ctx);
// Custom schema with validation and guardrails
const OrderBaggage = createSafeBaggageSchema(
{
orderId: { type: 'string', maxLength: 36 },
customerId: { type: 'string', hash: true },
tier: { type: 'enum', values: ['free', 'pro', 'enterprise'] as const },
},
{
prefix: 'order', // Keys: order.orderId, order.tier
redactPII: true, // Auto-redact email/phone/SSN patterns
hashHighCardinality: true, // Hash UUIDs/timestamps
},
);

Guardrails:

  • Size limits: maxKeyLength (default 64), maxValueLength (default 256)
  • PII detection: Regex patterns for email, phone, SSN auto-redacted
  • High-cardinality hashing: UUIDs and timestamps hashed via FNV-1a
  • Enum validation: Rejects values not in the defined set
  • Type coercion: Numbers/booleans properly serialized

Track distributed workflows with compensation support:

import { traceWorkflow, traceStep } from 'autotel/workflow';
export const orderSaga = traceWorkflow({
name: 'OrderSaga',
workflowId: (order) => order.id,
})((ctx) => async (order) => {
await traceStep({
name: 'ReserveInventory',
compensate: async (ctx, error) => {
await inventoryService.release(order.items); // Rollback
},
})((ctx) => async () => {
await inventoryService.reserve(order.items);
})();
await traceStep({
name: 'ChargePayment',
linkToPrevious: true, // Link to ReserveInventory span
compensate: async (ctx, error) => {
await paymentService.refund(order.id);
},
})((ctx) => async () => {
await paymentService.charge(order);
})();
});
// If ChargePayment fails, compensations run in reverse order

Key features:

  • traceWorkflow creates root span with workflow.name, workflow.id attributes
  • traceStep creates child spans with workflow.step.name, workflow.step.index
  • linkToPrevious: true creates span links for step sequencing
  • Compensations run in reverse order on failure
  • ctx.getWorkflowId(), ctx.getWorkflowName(), ctx.getStepIndex() context methods
  • WeakMap-based state isolation tied to span lifecycle

autotel/workflow uses AsyncLocalStorage, so it stays inside one process. When a workflow spans microservices, autotel/workflow-distributed carries the workflow identity over W3C baggage instead.

import {
traceDistributedWorkflow,
traceDistributedStep,
} from 'autotel/workflow-distributed';
// Service A starts the workflow
export const createOrder = traceDistributedWorkflow({
name: 'OrderFulfillment',
workflowIdFrom: (order) => order.id,
})((ctx) => async (order) => {
await publishToInventory(order); // workflow baggage rides the outbound headers
return { workflowId: ctx.workflowId };
});
// Service B continues it
export const reserveInventory = traceDistributedStep({
name: 'ReserveInventory',
extractBaggage: true,
})((ctx) => async (message) => {
await reserveItems(message.items); // ctx.workflowId matches Service A
});

createWorkflowHeaders() and parseWorkflowFromBaggage() give you manual control when the transport is not HTTP. getWorkflowProgress() and isInDistributedWorkflow() read the current state.

A webhook or payment callback can land hours after the request that triggered it. autotel/webhook stores the originating trace context under a correlation key and links the later callback span back to it.

import { trace } from 'autotel';
import { createParkingLot, InMemoryTraceContextStore } from 'autotel/webhook';
const parkingLot = createParkingLot({
store: new InMemoryTraceContextStore(),
defaultTTLMs: 24 * 60 * 60 * 1000,
});
export const initiatePayment = trace((ctx) => async (orderId: string) => {
await parkingLot.park(`payment:${orderId}`);
await stripe.createPaymentIntent({ metadata: { orderId } });
});
export const handleStripeWebhook = parkingLot.traceCallback({
name: 'stripe.webhook.payment_intent.succeeded',
correlationKeyFrom: (event) => `payment:${event.data.object.metadata.orderId}`,
})((ctx) => async (event) => {
// ctx.elapsedMs reports how long the callback took to arrive
await fulfillOrder(event.data.object);
});

Swap InMemoryTraceContextStore for a Redis-backed store in production so parked context survives a restart.

A correlation id is a stable 16-character join key that ties fragmented traces together. autotel/correlation-id generates one at a request boundary and reads it back anywhere in the same async scope.

import {
generateCorrelationId,
getCorrelationId,
runWithCorrelationId,
} from 'autotel/correlation-id';
await runWithCorrelationId(generateCorrelationId(), async () => {
getCorrelationId(); // available to every call in this async scope
await handleRequest();
});

See Configuration for wiring correlation ids into init().

autotel/decorators wraps class methods with tracing using TypeScript 5 decorator syntax. The span takes the method name by default.

import { Trace } from 'autotel/decorators';
class OrderService {
@Trace('order.create', { withMetrics: true })
async createOrder(data: OrderData) {
return db.orders.create(data);
}
@Trace() // span name: processPayment
async processPayment(orderId: string) {
return stripe.charge(orderId);
}
}

autotel/enrichers parses request and response headers into normalized span attributes for user agent, geolocation, and body size.

import { userAgent, geo, requestSize } from 'autotel/enrichers';
const ua = userAgent(req.headers);
if (ua) ctx.setAttribute('user_agent.browser', ua['user_agent.browser']);
const location = geo(req.headers);
if (location?.['geo.country']) {
ctx.setAttribute('geo.country', location['geo.country']);
}

autotel/drain-pipeline batches async work, retries with backoff, and flushes on an interval. Subscribers and exporters use it internally; reach for it when you build your own sink.

import { createDrainPipeline } from 'autotel/drain-pipeline';
const send = createDrainPipeline<LogEvent>({
batch: { size: 50, intervalMs: 5000 },
retry: { maxAttempts: 3, backoff: 'exponential' },
maxBufferSize: 1000,
})(async (batch) => {
await logService.send(batch);
});
send(event);
await send.flush();
await send.shutdown();