Configuration
Autotel supports standard OpenTelemetry environment variables for configuration. This enables zero-code configuration changes across environments.
Environment Variables
Section titled “Environment Variables”Service Configuration:
OTEL_SERVICE_NAME: Service name (maps toserviceininit())
Exporter Configuration:
OTEL_EXPORTER_OTLP_ENDPOINT: OTLP collector URL (e.g.http://localhost:4318)OTEL_EXPORTER_OTLP_PROTOCOL:http/json,http/protobuf, orgrpc(httpremains an alias for JSON)OTEL_EXPORTER_OTLP_HEADERS: Auth headers as comma-separatedkey=valuepairs
Resource Attributes:
OTEL_RESOURCE_ATTRIBUTES: Custom metadata tags as comma-separatedkey=valuepairs
Configuration Precedence
Section titled “Configuration Precedence”- Explicit
init()parameters: highest priority - YAML file:
autotel.yamlorAUTOTEL_CONFIG_FILEenv var - Environment variables:
OTEL_*,AUTOTEL_* - Built-in defaults: sensible defaults for development
init({ service: 'my-service', // Overrides YAML and OTEL_SERVICE_NAME endpoint: 'http://localhost:4318', // Single OTLP destination shorthand});For multi-backend OTLP fan-out, use destinations instead of overloading endpoint:
init({ service: 'my-service', logs: true, destinations: [ { endpoint: 'https://otlp-gateway-prod.grafana.net/otlp', headers: 'Authorization=Basic ...', }, { endpoint: 'https://api.honeycomb.io', headers: { 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY! }, signals: ['traces'], }, ],});OTLP wire protocol
Section titled “OTLP wire protocol”Autotel keeps the two OTLP/HTTP encodings distinct:
init() value |
Environment value | Wire format | Trace exporter |
|---|---|---|---|
http |
http/json |
OTLP/HTTP JSON | @opentelemetry/exporter-trace-otlp-http (included) |
http/protobuf |
http/protobuf |
OTLP/HTTP protobuf | @opentelemetry/exporter-trace-otlp-proto (optional peer) |
grpc |
grpc |
OTLP/gRPC | @opentelemetry/exporter-trace-otlp-grpc (optional peer) |
OTEL_EXPORTER_OTLP_PROTOCOL=http is accepted as the long-standing JSON
alias. http/protobuf is not interchangeable with http: it selects the
protobuf exporter and fails clearly at startup if that optional package is not
installed. Install the matching metric and log protobuf exporters as well when
those signals are enabled.
init({ service: 'my-service', endpoint: 'https://otel.example.com', protocol: 'http/protobuf',});HTTP endpoints may include a path prefix. Autotel preserves it when appending
the signal path, so an endpoint ending in /i sends traces to /i/v1/traces.
Process Handlers
Section titled “Process Handlers”A container gets SIGTERM, your process exits, and the last few seconds of
telemetry never leave the buffer. Importing autotel registers no process
listeners, so you opt in:
init({ service: 'my-app', processHandlers: true, // flush and exit on SIGTERM/SIGINT and fatal errors});true (or {}) installs the defaults. Pass an object to change one of them:
init({ service: 'my-app', processHandlers: { signals: ['SIGTERM', 'SIGINT'], // default fatalErrors: true, // flush on uncaughtException / unhandledRejection shutdownTimeoutMs: 2000, // give up waiting and exit anyway },});Autotel prints fatal errors to stderr before flushing. Registering an
uncaughtException listener overrides Node’s default stack trace, so without
that print a crash under fatalErrors would exit in silence.
Leave processHandlers off when your application already owns its lifecycle.
Applications with their own resource cleanup should call shutdown() from their
existing handler instead:
import { shutdown } from 'autotel';
process.on('SIGTERM', async () => { await Promise.all([closeDatabase(), shutdown()]); process.exit(0);});Two handlers fighting over the same signal is the failure mode this avoids.
Init Locking
Section titled “Init Locking”Framework plugins can lock init() to prevent accidental re-initialization from user code:
import { init, lockLogger, isLoggerLocked } from 'autotel';
// Framework plugin calls this after setting up instrumentationlockLogger();
// Subsequent init() calls are ignoredisLoggerLocked(); // trueSilent Mode
Section titled “Silent Mode”Suppress internal autotel diagnostic output while keeping exporters running:
init({ service: 'my-app', endpoint: 'http://localhost:4318', silent: true, // Suppress console output (default: false) minLevel: 'warn', // Only log warnings and errors (default: 'info')});Useful for platforms like GCP Cloud Run or AWS Lambda where stdout is managed externally.
spanEnrichers
Section titled “spanEnrichers”Span processors that decorate spans on the way out. Unlike spanProcessors,
which replaces the pipeline autotel builds from destinations, enrichers are
added to it:
init({ service: 'support-agent', destinations: [{ endpoint: 'https://collector.example.com' }], spanEnrichers: [langfuseCompatibility({ tags: ['production'] })],});Use them for anything that adds attributes rather than exporting: mapping
canonical attributes onto a backend’s own names, stamping a deployment id,
deriving one attribute from another. A processor passed as spanProcessors
would take the pipeline over and silently switch off every destination above it.
Enrichers run first, and outside the redaction, normalizer and filter wrappers. That ordering is deliberate: they decorate the real span, and Attribute Redaction downstream then sees the attributes they added rather than missing an enricher that copied a sensitive value into a new key.
Attribute Redaction
Section titled “Attribute Redaction”Automatically redact PII and sensitive data from span attributes for compliance:
init({ service: 'my-app', attributeRedactor: 'default', // 'default' | 'strict' | 'pci-dss'});Presets:
default: Emails, phone numbers, SSNs, credit cards (last 4), sensitive keys (password, token, etc.)strict: All of default plus JWTs, Bearer tokens, IBANs, API keys in valuespci-dss: Focused on payment card data
Custom configuration:
init({ service: 'my-app', attributeRedactor: { keyPatterns: [/password/i, /secret/i], // Keys to redact entirely valuePatterns: [ // Custom patterns with smart masking { name: 'customerId', pattern: /CUST-\d{8}/g, replacement: 'CUST-***' }, ], builtins: ['email', 'creditCard'], // Enable specific built-in patterns replacement: '[REDACTED]', // Default replacement (default) },});Built-in patterns: email, phone, creditCard, ipv4, jwt, bearer, iban
For redaction driven by portable rule files rather than presets, see Telemetry Policies.
Telemetry Policies
Section titled “Telemetry Policies”Portable, fail-open rules describing what telemetry to keep and how to transform
it, following OTEP 4738.
Point init() at a .json file or a directory of them:
init({ service: 'my-app', policies: './policies',});The path is watched, so policies can change without restarting the process. See Telemetry Policies for the supported subset of the specification.
YAML Configuration
Section titled “YAML Configuration”Create an autotel.yaml file in your project root:
service: name: my-service version: 1.0.0 environment: ${env:NODE_ENV:-development}
exporter: endpoint: ${env:OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318} protocol: http headers: x-honeycomb-team: ${env:HONEYCOMB_API_KEY} # Optional OTLP fan-out # destinations: # - endpoint: https://otlp-gateway-prod.grafana.net/otlp # headers: # Authorization: Basic ${env:GRAFANA_OTLP_BASIC_AUTH} # - endpoint: https://api.honeycomb.io # headers: # x-honeycomb-team: ${env:HONEYCOMB_API_KEY} # signals: # - traces
resource: deployment.environment: ${env:NODE_ENV:-development} team: backend
autoInstrumentations: - express - http - pino
# Telemetry Policies: a .json file or a directory of them, watched for changes# policies: ./policies
debug: falseKey features:
- Auto-discovery: Automatically loads
autotel.yamlorautotel.ymlfrom the current directory - Explicit path: Set
AUTOTEL_CONFIG_FILE=./config/otel.yamlto use a custom path - Environment variable substitution: Use
${env:VAR_NAME}or${env:VAR_NAME:-default} - Programmatic loading: Use
loadYamlConfigFromFile()fromautotel/yaml
Usage with autotel/auto (zero-config):
# Just create autotel.yaml and run:tsx --import autotel/auto src/index.tsProgrammatic loading:
import { loadYamlConfigFromFile } from 'autotel/yaml';import { init } from 'autotel';
const yamlConfig = loadYamlConfigFromFile('./config/otel.yaml');init({ ...yamlConfig, debug: true });See packages/autotel/autotel.yaml.example for a complete template.
Example Usage
Section titled “Example Usage”Development (local collector):
export OTEL_SERVICE_NAME=my-appexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318Production (Honeycomb):
export OTEL_SERVICE_NAME=my-appexport OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.ioexport OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=YOUR_API_KEYexport OTEL_RESOURCE_ATTRIBUTES=service.version=1.2.3,deployment.environment=productionProduction (Datadog):
export OTEL_SERVICE_NAME=my-appexport OTEL_EXPORTER_OTLP_ENDPOINT=https://http-intake.logs.datadoghq.comexport OTEL_EXPORTER_OTLP_HEADERS=DD-API-KEY=YOUR_API_KEYexport OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,team=backendProduction (Grafana + Honeycomb fan-out via init()):
init({ service: 'my-app', logs: true, destinations: [ { endpoint: 'https://otlp-gateway-prod.grafana.net/otlp', headers: 'Authorization=Basic ...', }, { endpoint: 'https://api.honeycomb.io', headers: { 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY! }, signals: ['traces'], }, ],});See packages/autotel/.env.example for a complete template.
Implementation Details
Section titled “Implementation Details”Environment variable resolution is handled in packages/autotel/src/env-config.ts. The resolver:
- Validates env var formats (URLs, enum values)
- Parses complex values (comma-separated key=value pairs)
- Provides type-safe config objects
YAML configuration is handled in packages/autotel/src/yaml-config.ts. The loader:
- Auto-discovers
autotel.yamlorautotel.ymlin the current directory - Supports
AUTOTEL_CONFIG_FILEenv var for custom paths - Substitutes
${env:VAR}and${env:VAR:-default}syntax in YAML values - Converts YAML structure to
AutotelConfigtype
Config merging happens in init() with the priority: explicit > yaml > env > defaults
Event Delivery Timing
Section titled “Event Delivery Timing”Events via track() are batched and delivered asynchronously. The default flush interval is 10 seconds with a batch size of 100 events. Use flush() before process exit or in serverless environments to ensure events aren’t lost.
Flushing Events
Section titled “Flushing Events”Before assertions in tests or process shutdown, manually flush the event queue:
import { getEventQueue } from 'autotel';
await getEventQueue()?.flush();Serverless / Scripts
Section titled “Serverless / Scripts”Always flush before the process exits:
import { track, getEventQueue } from 'autotel';
track('script.started', { scriptName: 'data-import' });// ... do work ...track('script.completed', { recordsProcessed: 1000 });
await getEventQueue()?.flush();Or use a process handler:
process.on('beforeExit', async () => { await getEventQueue()?.flush();});Testing
Section titled “Testing”When testing event tracking, flush before assertions:
import { track, getEventQueue } from 'autotel';import { expect, test } from 'vitest';
test('tracks user signup', async () => { track('user.signup', { userId: '123' });
// Flush events before assertions await getEventQueue()?.flush();
// Now verify events were sent to your mock/test subscriber expect(mockSubscriber.events).toContainEqual( expect.objectContaining({ name: 'user.signup' }), );});Queue Configuration
Section titled “Queue Configuration”| Setting | Default | Description |
|---|---|---|
maxSize |
50,000 | Maximum events in queue before dropping |
batchSize |
100 | Events per batch |
flushInterval |
10,000ms | Automatic flush interval |
maxRetries |
3 | Retry attempts for failed deliveries |
rateLimit |
100/sec | Maximum events per second |
Correlation ID
Section titled “Correlation ID”The correlation ID is a stable join key across events, logs, and spans. It is available via getCorrelationId() and getOrCreateCorrelationId() from autotel.
- Where it’s set: The first use of
getOrCreateCorrelationId()in an async context creates it; or set it at a boundary (e.g. HTTP handler, message processor) viarunWithCorrelationId(id, fn)orsetCorrelationId(id)fromautotel/correlation-id. - Stability: The same value is used for the duration of the same AsyncLocalStorage context (the same async chain). A new context (e.g. a new request) gets a new ID unless you propagate it (e.g. via baggage or Kafka headers).
- Public API:
getCorrelationId()andgetOrCreateCorrelationId()are exported fromautotel. For cross-service propagation, usesetCorrelationIdInBaggage()fromautotel/correlation-id.
import { getOrCreateCorrelationId } from 'autotel';
// Inside a request or message handler: same ID for the whole async chainconst correlationId = getOrCreateCorrelationId();Guarantee: Events tracked within a request context will have the same correlation_id as the HTTP response header. The ID is captured synchronously at track() call time, before any async delivery occurs.
Event payload and autotel context
Section titled “Event payload and autotel context”With events.includeTraceContext and events.traceUrl configured in init(), every track(name, props) call automatically includes correlation_id and trace_url: no wrappers needed.
When using track() or the Event class, subscribers receive an optional autotel context (e.g. correlation_id, trace_id, span_id, trace_url) when events.includeTraceContext is enabled in init(). Subscribers that support it (e.g. WebhookSubscriber) receive this via the third parameter to trackEvent and include it in the payload (e.g. as a top-level autotel field or merged into attributes), so consumers can correlate events with traces.
Event Attribute Types
Section titled “Event Attribute Types”EventAttributes accepts any JSON-serializable values including nested objects:
- Primitives:
string,number,boolean - Nested objects:
{ user: { id: '123', roles: ['admin'] } } - Arrays:
['tag1', 'tag2']
Nested objects are validated with a max depth of 3 levels (configurable). Circular references are replaced with '[CIRCULAR]'.