Skip to content

Configuration

Autotel supports standard OpenTelemetry environment variables for configuration. This enables zero-code configuration changes across environments.

Service Configuration:

  • OTEL_SERVICE_NAME: Service name (maps to service in init())

Exporter Configuration:

  • OTEL_EXPORTER_OTLP_ENDPOINT: OTLP collector URL (e.g. http://localhost:4318)
  • OTEL_EXPORTER_OTLP_PROTOCOL: http/json, http/protobuf, or grpc (http remains an alias for JSON)
  • OTEL_EXPORTER_OTLP_HEADERS: Auth headers as comma-separated key=value pairs

Resource Attributes:

  • OTEL_RESOURCE_ATTRIBUTES: Custom metadata tags as comma-separated key=value pairs
  1. Explicit init() parameters: highest priority
  2. YAML file: autotel.yaml or AUTOTEL_CONFIG_FILE env var
  3. Environment variables: OTEL_*, AUTOTEL_*
  4. 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'],
},
],
});

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.

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.

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 instrumentation
lockLogger();
// Subsequent init() calls are ignored
isLoggerLocked(); // true

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.

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.

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 values
  • pci-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.

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.

Create an autotel.yaml file in your project root:

autotel.yaml
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: false

Key features:

  • Auto-discovery: Automatically loads autotel.yaml or autotel.yml from the current directory
  • Explicit path: Set AUTOTEL_CONFIG_FILE=./config/otel.yaml to use a custom path
  • Environment variable substitution: Use ${env:VAR_NAME} or ${env:VAR_NAME:-default}
  • Programmatic loading: Use loadYamlConfigFromFile() from autotel/yaml

Usage with autotel/auto (zero-config):

Terminal window
# Just create autotel.yaml and run:
tsx --import autotel/auto src/index.ts

Programmatic 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.

Development (local collector):

Terminal window
export OTEL_SERVICE_NAME=my-app
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318

Production (Honeycomb):

Terminal window
export OTEL_SERVICE_NAME=my-app
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io
export OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=YOUR_API_KEY
export OTEL_RESOURCE_ATTRIBUTES=service.version=1.2.3,deployment.environment=production

Production (Datadog):

Terminal window
export OTEL_SERVICE_NAME=my-app
export OTEL_EXPORTER_OTLP_ENDPOINT=https://http-intake.logs.datadoghq.com
export OTEL_EXPORTER_OTLP_HEADERS=DD-API-KEY=YOUR_API_KEY
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,team=backend

Production (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.

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.yaml or autotel.yml in the current directory
  • Supports AUTOTEL_CONFIG_FILE env var for custom paths
  • Substitutes ${env:VAR} and ${env:VAR:-default} syntax in YAML values
  • Converts YAML structure to AutotelConfig type

Config merging happens in init() with the priority: explicit > yaml > env > defaults

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.

Before assertions in tests or process shutdown, manually flush the event queue:

import { getEventQueue } from 'autotel';
await getEventQueue()?.flush();

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();
});

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' }),
);
});
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

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) via runWithCorrelationId(id, fn) or setCorrelationId(id) from autotel/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() and getOrCreateCorrelationId() are exported from autotel. For cross-service propagation, use setCorrelationIdInBaggage() from autotel/correlation-id.
import { getOrCreateCorrelationId } from 'autotel';
// Inside a request or message handler: same ID for the whole async chain
const 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.

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.

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]'.