Skip to content

Migration from OpenTelemetry

Typical migration reduces SDK setup from 30+ lines to 5-10 lines and replaces manual span lifecycle management with functional wrappers.

Follow these 4 steps:

Terminal window
npm install autotel
# or
pnpm add autotel

Step 2: Find Your Current OpenTelemetry Setup

Section titled “Step 2: Find Your Current OpenTelemetry Setup”

Look for one of these patterns in your code:

Pattern A: Environment variables + NODE_OPTIONS

Terminal window
# In your .env, docker-compose.yml, or startup script
NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register"
OTEL_EXPORTER_OTLP_ENDPOINT="http://your-collector:4318"
OTEL_SERVICE_NAME="your-service"

Pattern B: Manual SDK initialization

// In instrumentation.ts, tracing.ts, or app.ts
const sdk = new opentelemetry.NodeSDK({
serviceName: 'your-service',
traceExporter: new OTLPTraceExporter({ url: '...' }),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();

Heads up: Autotel lazily loads @opentelemetry/auto-instrumentations-node when you set integrations. Keep that package installed (or add it now) so the same auto-instrumentations you used with NODE_OPTIONS remain available.

If you found Pattern A (environment variables):

  1. Remove the NODE_OPTIONS environment variable
  2. Create a new file instrumentation.ts (or add to existing entry point):
import { init } from 'autotel';
init({
service: process.env.OTEL_SERVICE_NAME || 'your-service',
endpoint:
process.env.OTLP_ENDPOINT ??
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
'http://localhost:4318',
integrations: true, // Requires @opentelemetry/auto-instrumentations-node
});
  1. Import this file at the very top of your entry point:
// At the top of app.ts, server.ts, or index.ts
import './instrumentation'; // ← Add this FIRST
// Rest of your imports
import express from 'express';
// ...

If you found Pattern B (manual SDK):

  1. Replace your entire SDK setup with:
import { init, shutdown } from 'autotel';
init({
service: 'your-service', // Copy from your NodeSDK config
endpoint: 'http://your-collector:4318', // Copy from your exporter URL
integrations: true,
});
// Replace sdk.shutdown() calls
process.on('SIGTERM', shutdown);
  1. Remove these imports (you don’t need them anymore):
// DELETE THESE:
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
// etc.
  1. Start your app:

    Terminal window
    npm start
  2. Check your observability backend (Honeycomb, Datadog, Grafana, etc.):

    • Traces should appear
    • Service name matches your configuration
    • Auto-instrumentations working (HTTP, database calls, etc.)
  3. Trigger an error to verify error traces appear

  • Auto-instrumentation uses the same libraries
  • OTLP endpoint unchanged
  • Tail sampling replaces head sampling (10% baseline, 100% errors/slow requests)
  • Rate limiting and circuit breakers added
  • PII redaction available

Replace manual spans with trace():

// OLD: Manual span management
const tracer = trace.getTracer('my-app');
async function createUser(data) {
const span = tracer.startSpan('createUser');
try {
const user = await db.users.create(data);
span.end();
return user;
} catch (error) {
span.recordException(error);
span.end();
throw error;
}
}
// NEW: Automatic lifecycle
import { trace, withTracing } from 'autotel';
const createUser = trace(async (data) => {
return await db.users.create(data);
});

Automatic log correlation:

import { createLogger } from 'autotel/logger';
const logger = createLogger('my-service');
logger.info('Request processed'); // Trace ID included automatically

See sections below for pattern-by-pattern migrations, custom sampling, and edge cases.


OpenTelemetry Pattern Autotel Equivalent What Changes
NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register" init({ integrations: true }) Programmatic configuration
new NodeSDK({ ... }) init({ ... }) Reduced boilerplate (30+ → 5 lines)
tracer.startSpan() + span.end() trace(fn) or trace(name, fn) Automatic span lifecycle
Manual log correlation autotel/logger Automatic trace context injection
Head sampling Tail sampling (default) Sample 100% of errors/slow requests
Custom span processor Built-in rate limiters, circuit breakers Rate limiting, circuit breakers

Pattern 1: Environment Variables + Auto-Instrumentation

Section titled “Pattern 1: Environment Variables + Auto-Instrumentation”

Before:

Terminal window
OTEL_TRACES_EXPORTER="otlp" \
OTEL_METRICS_EXPORTER="otlp" \
OTEL_LOGS_EXPORTER="otlp" \
OTEL_NODE_RESOURCE_DETECTORS="env,host,os" \
OTEL_RESOURCE_ATTRIBUTES="service.name=my-service,service.namespace=production,deployment.environment=prod" \
OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com:4318 \
NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register" \
node app.js

After:

import { init } from 'autotel';
init({
service: 'my-service',
environment: 'prod',
endpoint: 'https://collector.example.com:4318',
integrations: true, // Enables all auto-instrumentations
resourceAttributes: {
'service.namespace': 'production',
},
});

Need advanced resource detection (env/host/os)? Reuse whichever detectors you already configured (for example via @opentelemetry/resource-detector-*), build a Resource with them, and pass it through the resource option.

What Changed:

  • No NODE_OPTIONS flag
  • Programmatic configuration (change without restart)
  • Type-safe configuration
  • Includes rate limiting, circuit breakers, PII redaction
  • Tail sampling (10% baseline, 100% errors/slow requests)

Before:

const process = require('process');
const opentelemetry = require('@opentelemetry/sdk-node');
const {
getNodeAutoInstrumentations,
} = require('@opentelemetry/auto-instrumentations-node');
const {
OTLPTraceExporter,
} = require('@opentelemetry/exporter-trace-otlp-http');
const {
OTLPMetricExporter,
} = require('@opentelemetry/exporter-metrics-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const {
ATTR_SERVICE_NAME,
ATTR_SERVICE_NAMESPACE,
} = require('@opentelemetry/semantic-conventions');
const resource = Resource.default().merge(
new Resource({
[ATTR_SERVICE_NAME]: 'my-service',
[ATTR_SERVICE_NAMESPACE]: 'production',
}),
);
const traceExporter = new OTLPTraceExporter({
url: 'https://collector.example.com:4318/v1/traces',
});
const metricReader = new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: 'https://collector.example.com:4318/v1/metrics',
}),
exportIntervalMillis: 60000,
});
const sdk = new opentelemetry.NodeSDK({
resource,
traceExporter,
metricReader,
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
process.on('SIGTERM', () => {
sdk
.shutdown()
.then(() => console.log('Tracing terminated'))
.catch((error) => console.log('Error terminating tracing', error))
.finally(() => process.exit(0));
});

After:

import { init, shutdown } from 'autotel';
init({
service: 'my-service',
environment: 'production',
endpoint: 'https://collector.example.com:4318',
integrations: true,
metrics: true, // Enabled by default; set false to disable
});
process.on('SIGTERM', shutdown);

Need to customize metric export intervals or swap exporters? Provide your own metricReader, just like you would with vanilla OpenTelemetry:

import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
const metricReader = new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: 'https://collector.example.com:4318/v1/metrics',
}),
exportIntervalMillis: 60000,
});
init({
service: 'my-service',
endpoint: 'https://collector.example.com:4318',
metricReader,
});

What Changed:

  • Automatic exporter configuration (traces, metrics, logs)
  • Automatic resource detection and merging
  • Simplified shutdown (single function)
  • Includes rate limiting, circuit breakers, tail sampling
  • No manual exporter/reader instantiation

Before:

import { trace, withTracing } from '@opentelemetry/api';
import { SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('my-app');
async function createUser(data) {
const span = tracer.startSpan('createUser', {
attributes: {
'user.email': data.email,
},
});
try {
span.setAttribute('user.id', data.id);
const user = await db.users.create(data);
span.setStatus({ code: SpanStatusCode.OK });
return user;
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
span.recordException(error);
throw error;
} finally {
span.end();
}
}
// Calling the function
const user = await createUser({ email: 'user@example.com', id: '123' });

After:

import { trace, withTracing } from 'autotel';
// Factory pattern (receives context parameter)
const createUser = withTracing({})((ctx) => async (data) => {
// Span automatically created with function name as operation
ctx.setAttribute('user.email', data.email);
ctx.setAttribute('user.id', data.id);
const user = await db.users.create(data);
return user;
// Span automatically ended, errors automatically recorded
});
// Calling the function (same API)
const user = await createUser({ email: 'user@example.com', id: '123' });

Alternative: Direct pattern (when you don’t need context)

import { trace, withTracing } from 'autotel';
// Direct pattern (no context needed)
const getUser = trace(async (id) => {
return await db.users.findById(id);
});
// Autotel wraps the plain function and handles lifecycle
const user = await getUser('123');

What Changed:

  • No manual span.start() / span.end()
  • Automatic error handling (no try/catch needed for telemetry)
  • No manual status codes
  • Context propagation handled automatically
  • Function name used as span name (customizable via @operationName decorator)

Before:

import pino from 'pino';
import { trace, withTracing, context } from '@opentelemetry/api';
const logger = pino();
async function handleRequest(req) {
const span = trace.getActiveSpan();
const spanContext = span?.spanContext();
logger.info(
{
traceId: spanContext?.traceId,
spanId: spanContext?.spanId,
traceFlags: spanContext?.traceFlags,
},
'Processing request',
);
// Business logic...
}

After:

import { createLogger } from 'autotel/logger';
import { trace, withTracing } from 'autotel';
const logger = createLogger({ name: 'my-service' });
const handleRequest = trace(async (req) => {
// Trace context automatically added to every log
logger.info('Processing request');
// Business logic...
});

Log Output (automatic trace correlation):

{
"level": "info",
"msg": "Processing request",
"trace_id": "a1b2c3d4e5f6g7h8",
"span_id": "1234567890abcdef",
"trace_flags": "01"
}

What Changed:

  • Automatic trace context injection
  • No manual span context extraction
  • Works with structured logging (JSON)
  • Supports pino and winston

Before:

import {
TraceIdRatioBasedSampler,
ParentBasedSampler,
} from '@opentelemetry/sdk-trace-base';
const sdk = new opentelemetry.NodeSDK({
// Sample 10% of all requests
sampler: new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.1),
}),
// ...
});

Problem: With head sampling, you lose 90% of error traces and slow requests.

After:

import { AdaptiveSampler, init } from 'autotel';
init({
service: 'my-service',
endpoint: 'https://collector.example.com:4318',
sampler: new AdaptiveSampler({
baselineSampleRate: 0.1, // Sample 10% of normal requests
slowThresholdMs: 1000, // >1s is "slow"
alwaysSampleErrors: true, // Sample 100% of errors
alwaysSampleSlow: true, // Sample 100% of slow requests
}),
});

Custom Sampler (advanced):

import { init, type Sampler, type SamplingContext } from 'autotel';
class CustomSampler implements Sampler {
shouldSample(context: SamplingContext): boolean {
const firstArg = context.args[0] as { user?: { authenticated?: boolean } };
if (firstArg?.user?.authenticated) {
return true;
}
return Math.random() < 0.05;
}
}
init({
service: 'my-service',
sampler: new CustomSampler(),
});

What Changed:

  • Tail sampling instead of head sampling
  • Sampling decisions can consider operation success/failure and duration
  • Never lose error traces or slow requests
  • Dramatically better observability at the same cost

Benefits:

  • Capture 100% of errors with only 10-20% of total volume
  • Inspect function inputs (args/metadata) plus duration before deciding
  • Adaptive sampling based on conditions

Before:

import { metrics } from '@opentelemetry/api';
const meter = metrics.getMeter('my-app');
const requestCounter = meter.createCounter('http.requests', {
description: 'Total HTTP requests',
});
function handleRequest(req) {
requestCounter.add(1, {
method: req.method,
route: req.route,
});
}

After:

import { createCounter } from 'autotel';
const requestCounter = createCounter('http.requests', {
description: 'Total HTTP requests',
});
function handleRequest(req) {
requestCounter.add(1, {
method: req.method,
route: req.route,
});
}

What Changed:

  • Simpler helper: createCounter() instead of meter.createCounter()
  • Built-in helpers for common patterns
  • Automatic meter registration

OpenTelemetry API/SDK Autotel Equivalent Notes
new NodeSDK({...}) init({...}) Simplified configuration
tracer.startSpan() + span.end() trace(fn), trace(name, fn) Automatic lifecycle
span.setAttribute() ctx.setAttribute() Same API, different context
span.setStatus() Automatic Based on exception/return
span.recordException() Automatic All errors auto-recorded
ParentBasedSampler sampler: new AdaptiveSampler() Tail sampling instead
getNodeAutoInstrumentations() integrations: true Same libraries instrumented
sdk.shutdown() shutdown() Graceful shutdown
Manual log correlation autotel/logger Built-in correlation
Manual context propagation Automatic Works out of the box
OTEL_EXPORTER_OTLP_ENDPOINT endpoint Config over env vars

Migrating OpenTelemetry Collector Configuration

Section titled “Migrating OpenTelemetry Collector Configuration”

Your collector config doesn’t need to change. Autotel uses standard OTLP protocol.

Before (collector config):

receivers:
otlp:
protocols:
http:
endpoint: '0.0.0.0:4318'
exporters:
awss3:
s3uploader:
region: us-east-1
s3_bucket: my-telemetry-bucket
processors:
batch:
timeout: 10s
send_batch_size: 32768
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [awss3]

After:

import { init } from 'autotel';
init({
service: 'my-service',
endpoint: 'http://collector:4318', // Same OTLP endpoint
});

Before:

import { InstrumentationBase } from '@opentelemetry/instrumentation';
class MyCustomInstrumentation extends InstrumentationBase {
init() {
// Custom instrumentation logic
}
}
const sdk = new NodeSDK({
instrumentations: [
getNodeAutoInstrumentations(),
new MyCustomInstrumentation(),
],
});

After:

import { init } from 'autotel';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
init({
service: 'my-service',
integrations: true,
});
// Register custom instrumentations separately
registerInstrumentations({
instrumentations: [new MyCustomInstrumentation()],
});

Before:

import { context, trace, withTracing } from '@opentelemetry/api';
const span = tracer.startSpan('parent');
const ctx = trace.setSpan(context.active(), span);
await context.with(ctx, async () => {
// Child operations will inherit this context
await doSomething();
});
span.end();

After:

import { trace, withTracing } from 'autotel';
const parent = trace(async () => {
// Context propagation automatic
await doSomething();
});
await parent();

Context propagation works automatically with trace(), span(), and instrument().

Before:

import { InMemorySpanExporter } from '@opentelemetry/sdk-trace-base';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
const exporter = new InMemorySpanExporter();
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();
// Run tests
await myFunction();
// Assert on spans
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);

After:

import { InMemorySpanExporter } from '@opentelemetry/sdk-trace-base';
import { init } from 'autotel';
const exporter = new InMemorySpanExporter();
init({
service: 'test',
spanExporter: exporter, // Use in-memory exporter for testing
});
await myFunction();
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
expect(spans[0].name).toBe('myFunction');

Use this checklist for a smooth migration:

  • Review current OpenTelemetry configuration (SDK setup, instrumentations, exporters)
  • Identify custom instrumentations or span processors
  • Note current sampling strategy (head vs. tail)
  • Document custom attributes and context propagation patterns
  • Check if using non-OTLP exporters (Jaeger, Zipkin)
  • Install autotel: npm install autotel
  • Install adapters if needed: npm install autotel-subscribers
  • For edge runtimes: npm install autotel-edge
  • Keep/install auto-instrumentations: npm install @opentelemetry/auto-instrumentations-node
  • Replace NODE_OPTIONS environment variable setup
  • Replace new NodeSDK({...}) with init({...})
  • Migrate configuration options (service name, exporters, resource attributes)
  • Enable auto-instrumentation: integrations: true (requires @opentelemetry/auto-instrumentations-node)
  • Configure tail sampling (baseline, error, slow request rates)
  • Replace sdk.shutdown() with shutdown() in process handlers
  • Find all tracer.startSpan() + span.end() patterns
  • Replace with the trace(fn) / trace(name, fn) functional wrapper
  • Convert span.setAttribute() to ctx.setAttribute()
  • Remove manual error handling for telemetry (keep business error handling)
  • Test that span hierarchy is preserved
  • Replace manual trace context injection
  • Use createLogger() from autotel/logger
  • Verify trace/span IDs appear in log output
  • Update log parsing/indexing if format changed
  • Replace test span exporters with InMemorySpanExporter from @opentelemetry/sdk-trace-base
  • Update test assertions (span names, attributes)
  • Verify context propagation in tests
  • Run full test suite
  • Verify spans appear in your observability backend (Honeycomb, Datadog, etc.)
  • Check span attributes match expectations
  • Verify error spans are captured (trigger an error and check)
  • Verify slow requests are captured (check tail sampling)
  • Monitor for any missing instrumentations
  • Deploy to staging environment first
  • Monitor for increased/decreased span volume (tail sampling may change volume)
  • Verify no performance regressions (rate limiting should prevent issues)
  • Check error rates and success rates
  • Gradually roll out to production (canary, blue/green, etc.)
  • Tune sampling rates based on actual traffic
  • Configure rate limiting thresholds if needed
  • Add custom samplers for specific use cases
  • Enable PII redaction if handling sensitive data
  • Configure circuit breaker thresholds

Before (vanilla OpenTelemetry):

// File: instrumentation.ts (30+ lines)
const opentelemetry = require('@opentelemetry/sdk-node');
const {
getNodeAutoInstrumentations,
} = require('@opentelemetry/auto-instrumentations-node');
const {
OTLPTraceExporter,
} = require('@opentelemetry/exporter-trace-otlp-http');
const { Resource } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');
const resource = Resource.default().merge(
new Resource({
[ATTR_SERVICE_NAME]: 'my-service',
}),
);
const sdk = new opentelemetry.NodeSDK({
resource,
traceExporter: new OTLPTraceExporter({
url: 'http://collector:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
// File: user-service.ts (25+ lines for a single function)
import { trace, withTracing } from '@opentelemetry/api';
import { SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('user-service');
async function createUser(data) {
const span = tracer.startSpan('createUser');
try {
span.setAttribute('user.email', data.email);
const user = await db.users.create(data);
span.setStatus({ code: SpanStatusCode.OK });
return user;
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.recordException(error);
throw error;
} finally {
span.end();
}
}

After (autotel):

// File: instrumentation.ts (5 lines)
import { init } from 'autotel';
init({
service: 'my-service',
endpoint: 'http://collector:4318',
integrations: true,
});
// File: user-service.ts (7 lines)
import { trace, withTracing } from 'autotel';
const createUser = withTracing({})((ctx) => async (data) => {
ctx.setAttribute('user.email', data.email);
return await db.users.create(data);
});