Why OpenTelemetry for Node.js
OpenTelemetry is the cross-language standard for tracing, metrics, and logs. For Node.js it replaces the per-vendor APM agents that used to monkey-patch http and http2 at startup. With OTel you get the same instrumentation surface as Python, Go, Java, or .NET, and your telemetry travels in a single wire format — OTLP — that any backend (including Telemtra) can read.
The Node.js auto-instrumentation library covers the most common libraries: Express, NestJS, Fastify, Koa, GraphQL, Apollo, gRPC, AWS SDK, Prisma, Sequelize, TypeORM, pg, mysql, mysql2, mongodb, redis, ioredis, kafkajs, amqplib, undici, http, https. After auto-instrumentation is enabled, your service shows up in Telemtra's Service Map with full incoming and outgoing edges without any code changes.
Install the OpenTelemetry Node.js SDK
You need the SDK, OTLP exporter, and the auto-instrumentations bundle. For the OTLP transport, pick HTTP/protobuf for the easiest setup or gRPC for high throughput:
npm install \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/exporter-metrics-otlp-http \
@opentelemetry/exporter-logs-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions Where this goes in your project
Typical Node.js project layout after wiring up Telemtra. The only new file is tracing.js, and you change one line of your start command:
my-service/
├── src/
│ └── server.js # your existing Express/Nest/Fastify app (unchanged)
├── tracing.js # NEW: OpenTelemetry bootstrap (the snippet in the next section)
├── package.json # add a "start" script that preloads tracing.js
└── .env # TELEMTRA_API_KEY and OTEL_SERVICE_NAME 1. Save the bootstrap as tracing.js at the project root (see the snippet in the next section).
2. Update package.json scripts so Node preloads tracing.js before your app:
{
"scripts": {
"start": "node --require ./tracing.js src/server.js",
"dev": "node --require ./tracing.js --watch src/server.js"
}
} 3. Set environment variables in .env or your hosting provider:
TELEMTRA_API_KEY=tk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
OTEL_SERVICE_NAME=checkout-api
NODE_ENV=production You do not import OpenTelemetry from your app code. --require ensures the SDK initializes before any of your imports run, which is required for auto-instrumentation to patch modules at load time. If you skip this step and import OTel from inside server.js, instrumentation will fire late and you will see partial spans.
For ESM projects, use --import ./tracing.mjs in place of --require ./tracing.js. For TypeScript, compile tracing.ts alongside your app or run with tsx --import ./tracing.mjs.
Bootstrap auto-instrumentation
Create a tracing.js file that initializes the SDK before your app code loads, then require it as the first argument to node:
// tracing.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');
const headers = { 'x-api-key': process.env.TELEMTRA_API_KEY };
const url = 'https://ingest.telemtra.io';
const sdk = new NodeSDK({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'api',
'deployment.environment': process.env.NODE_ENV || 'development',
}),
traceExporter: new OTLPTraceExporter({ url: `${url}/v1/traces`, headers }),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({ url: `${url}/v1/metrics`, headers }),
exportIntervalMillis: 10_000,
}),
logRecordProcessor: new BatchLogRecordProcessor(
new OTLPLogExporter({ url: `${url}/v1/logs`, headers })
),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start(); Start the app with the tracing file preloaded:
TELEMTRA_API_KEY=your_key \
OTEL_SERVICE_NAME=checkout-api \
node --require ./tracing.js server.js That is the entire setup. Express routes, outgoing HTTP calls, Postgres queries, and Redis commands all start appearing in Telemtra within seconds of the first request.
Pointing the exporter at Telemtra
Telemtra accepts OTLP HTTP/protobuf on https://ingest.telemtra.io and OTLP gRPC on ingest.telemtra.io:443. Use HTTP when your app exports directly; use gRPC when you run a Collector sidecar and want a persistent connection between the Collector and Telemtra.
Authentication is a single header — x-api-key — set per exporter. Keep the key in an environment variable, never in source.
Manual spans for business logic
Auto-instrumentation handles framework and library boundaries. For business operations worth tracing on their own, add manual spans:
const { trace, SpanStatusCode } = require('@opentelemetry/api');
const tracer = trace.getTracer('checkout-api');
async function submitOrder(orderId, totalCents) {
return tracer.startActiveSpan('checkout.submit_order', async (span) => {
span.setAttribute('order.id', orderId);
span.setAttribute('order.total_cents', totalCents);
try {
await chargeCard(orderId, totalCents);
await writeLedger(orderId);
await sendReceipt(orderId);
span.setStatus({ code: SpanStatusCode.OK });
} catch (err) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
throw err;
} finally {
span.end();
}
});
} Auto-instrumented HTTP and DB calls inside chargeCard, writeLedger, and sendReceipt automatically nest under the parent span. The result in Telemtra is a waterfall that shows exactly which downstream operation pushed past your latency budget.
Send pino, winston, or bunyan logs as OTLP
Most Node.js apps already use pino, winston, or bunyan. OpenTelemetry's log API can either ingest those logger outputs or you can install a transport that ships log records as OTLP directly.
Pino
const pino = require('pino');
const { OpenTelemetryTransportV3 } = require('@opentelemetry/instrumentation-pino');
const logger = pino({
transport: { target: '@opentelemetry/instrumentation-pino/transport' },
});
logger.info({ orderId: 'ord_123' }, 'order accepted'); Winston
const winston = require('winston');
const { OpenTelemetryTransportV3 } = require('@opentelemetry/winston-transport');
const logger = winston.createLogger({
level: 'info',
transports: [new OpenTelemetryTransportV3()],
});
logger.info('order accepted', { orderId: 'ord_123' }); Either way, every log produced inside an active span is correlated with that span's trace ID, so you can jump from a slow trace in Telemtra to the exact logs it produced.
Metrics
const { metrics } = require('@opentelemetry/api');
const meter = metrics.getMeter('checkout-api');
const ordersTotal = meter.createCounter('orders_total', {
description: 'Number of orders submitted',
});
const checkoutLatency = meter.createHistogram('checkout_latency_ms', {
description: 'Checkout end-to-end latency',
unit: 'ms',
});
ordersTotal.add(1, { plan: 'team', region: 'eu-west-1' });
checkoutLatency.record(348, { region: 'eu-west-1' }); Telemtra can chart these as p50, p90, p95, or p99 lines, and trigger threshold alerts when latency degrades.
Framework-specific notes
Express
Auto-instrumented out of the box. Route templates (/users/:id) are preserved as span names so URL cardinality stays under control.
NestJS
Register @opentelemetry/instrumentation-nestjs-core for guard, interceptor, and pipe spans alongside Express or Fastify.
Fastify
Use @opentelemetry/instrumentation-fastify. Hook lifecycle spans (onRequest, preHandler, onSend) are captured.
Next.js
Enable instrumentation in next.config.js with experimental.instrumentationHook, then create instrumentation.ts that imports the tracing bootstrap. Server-side rendering, API routes, and middleware are all traced.
Lambda and serverless
For AWS Lambda use the OpenTelemetry Lambda Layer; for Vercel and Cloudflare Workers, use the OTLP HTTP exporter with very short timeouts so the function does not block on flush. With Telemtra as the backend you can OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.telemtra.io and flush before return.
ESM and TypeScript
For ESM, use node --import ./tracing.mjs server.mjs. The auto-instrumentations bundle supports both CJS and ESM as of recent versions. For TypeScript, write tracing.ts the same way and compile it with the rest of your project, or run via tsx/ts-node.
Troubleshooting
- Spans missing from auto-instrumentation. Confirm
tracing.jsis loaded with--requirebefore the app, not imported later. OTel must patch modules before they are required. - 403 from exporter. The API key header must be
x-api-key, lowercase, and the value must match an active workspace key. - Logs not arriving. Confirm the log processor is wired into the SDK and the log exporter URL ends in
/v1/logs. - High span volume. Add a tail sampler in the Collector or use the OTel
ParentBasedSamplerwith a probability sampler at the SDK level.
For deeper transport detail see What is OTLP? and the Collector quickstart. New to Telemtra? The quickstart walks the minimum config in 60 seconds. To go live, create a workspace and paste your API key into the snippet above.
OpenTelemetry Node.js FAQ
Does OpenTelemetry Node.js work with Express, NestJS, Fastify, and Koa?
Yes. The auto-instrumentations-node package covers Express, NestJS, Fastify, Koa, GraphQL, gRPC, http, https, undici, Prisma, Sequelize, TypeORM, pg, mysql2, mongodb, redis, ioredis, kafkajs, amqplib, and AWS SDK. Register it with --require and incoming and outgoing calls become spans without code changes.
Does it work with TypeScript and ESM?
Yes. For CommonJS, run node --require ./tracing.js app.js. For ESM, run node --import ./tracing.mjs server.mjs. The auto-instrumentations bundle supports both CJS and ESM in recent versions; TypeScript projects can compile tracing.ts alongside the app or run via tsx/ts-node.
How do I send pino, winston, or bunyan logs through OpenTelemetry?
Each logger has a matching OTel bridge: @opentelemetry/instrumentation-pino, instrumentation-winston, and instrumentation-bunyan. Once registered, every log call becomes an OTLP log record with the active trace_id and span_id attached so logs link back to their trace in Telemtra.
What about AWS Lambda and serverless?
Use the AWS Lambda layer for OpenTelemetry or the @opentelemetry/instrumentation-aws-lambda package. Cold starts add a small latency cost on the first invocation; warm invocations are negligible. For sustained traffic, deploy a Collector sidecar to batch exports.
OTLP HTTP or gRPC for Node.js?
Default to HTTP/protobuf via @opentelemetry/exporter-trace-otlp-http - it works through any HTTP proxy and is the easiest setup. Use gRPC for high-throughput services where the persistent connection saves bytes. Both send the same OTLP data to Telemtra.
Further reading
- Telemtra quickstart — minimum Node.js config and snippets for every other supported stack.
- opentelemetry.io — JavaScript documentation
- opentelemetry.io — JavaScript zero-code instrumentation
- github.com/open-telemetry/opentelemetry-js — SDK source
- github.com/open-telemetry/opentelemetry-js-contrib — auto-instrumentations
- OpenTelemetry semantic conventions