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 } 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) | 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 } 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 } from 'autotel';
// Factory pattern (receives context parameter)
const createUser = trace((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 } from 'autotel';
// Direct pattern (no context needed)
const getUser = trace(async (id) => {
return await db.users.findById(id);
});
// Autotel auto-detects the pattern 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, 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 } 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/metrics';
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) | 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 } 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 } 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 trace(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 } 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 } from 'autotel';
const createUser = trace((ctx) => async (data) => {
ctx.setAttribute('user.email', data.email);
return await db.users.create(data);
});