OpenTelemetry for JavaScript: Practical Observability in 2026

#1OpenTelemetry in JavaScript: where tracing actually helps
What we tested: We evaluated the tools and techniques described in this article using real developer workflows. All browser-based tools on this site run locally, your data never leaves your machine.
OpenTelemetry becomes useful when logs tell you what happened but not where the time went.
The practical side of tracing Node.js and Next.js apps is getting setup right, adding spans where they matter, and reading traces well enough to find the bottleneck.
#2Why Observability vs Logging
Logging tells you what happened. Tracing tells you where time went and why.
A log says: [ERROR] Payment failed for user 123.
A trace says:
- Request received → 0ms
- Auth middleware → 2ms
- Stripe API call → 847ms ← the bottleneck
- DB write → 4ms
- Response sent → 856ms total
With tracing, you know to look at the Stripe API call. Without it, you search logs for 847ms before finding it.
#2Core OTel Concepts
Trace, the complete lifecycle of a single request across all services, from entry point to final response.
Span, a single unit of work within a trace. A trace is a tree of spans. Each span has a name, start time, duration, and optional attributes.
Context propagation, how spans "know" they belong to the same trace as spans in other services. OTel propagates a trace ID via the traceparent header.
Exporter, sends trace data to a backend (Jaeger, Grafana Tempo, DataDog, Honeycomb, etc.).
#2Node.js Auto-Instrumentation
OTel auto-instrumentation patches popular libraries (Express, Fastify, pg, mysql2, ioredis, http, fetch) to automatically create spans without code changes.
#3Install
npm install \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions#3Setup file, must load before everything else
// instrumentation.js, loaded FIRST via --require
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'my-api',
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4318/v1/traces',
headers: process.env.OTEL_EXPORTER_HEADERS
? JSON.parse(process.env.OTEL_EXPORTER_HEADERS)
: {},
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false }, // too noisy
}),
],
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0));
});#3Start with instrumentation
# node
node --require ./instrumentation.js server.js
# package.json
{
"scripts": {
"start": "node --require ./instrumentation.js dist/server.js",
"dev": "tsx --require ./instrumentation.js src/server.ts"
}
}Now every Express route, Postgres query, Redis call, and outbound HTTP request automatically creates spans.
#2Adding Custom Spans for Business Logic
Auto-instrumentation covers infrastructure. You add manual spans for the business logic that matters:
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('my-api', '1.0.0');
async function processPayment(userId: string, amount: number) {
// Create a span for this business operation
return tracer.startActiveSpan('payment.process', async (span) => {
try {
// Add attributes, these appear in your trace UI
span.setAttributes({
'payment.user_id': userId,
'payment.amount': amount,
'payment.currency': 'USD',
});
const user = await db.users.findById(userId);
span.setAttribute('payment.user_plan', user.plan);
// Nested span for the Stripe call
const chargeResult = await tracer.startActiveSpan('stripe.charge', async (stripeSpan) => {
stripeSpan.setAttribute('stripe.amount_cents', amount * 100);
const result = await stripe.charges.create({
amount: amount * 100,
currency: 'usd',
customer: user.stripeCustomerId,
});
stripeSpan.setAttribute('stripe.charge_id', result.id);
stripeSpan.end();
return result;
});
span.setAttribute('payment.charge_id', chargeResult.id);
span.setStatus({ code: SpanStatusCode.OK });
return chargeResult;
} catch (error) {
// Record error details in the span
span.recordException(error as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: (error as Error).message
});
throw error;
} finally {
span.end(); // always end the span
}
});
}#2Exporting Traces, Backends
#3Grafana Cloud (free tier, 50GB/month)
# .env
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp/v1/traces
OTEL_EXPORTER_HEADERS='{"Authorization":"Basic BASE64_ENCODED_USER:TOKEN"}'Get your endpoint and token from: Grafana Cloud → Connections → OpenTelemetry.
#3Jaeger (local, zero setup)
# Run Jaeger with Docker
docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest
# .env
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
# Open UI
open http://localhost:16686#3Honeycomb (free tier, 20M events/month)
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io/v1/traces
OTEL_EXPORTER_HEADERS='{"x-honeycomb-team":"YOUR_API_KEY","x-honeycomb-dataset":"my-api"}'#2Next.js Integration (App Router)
Next.js 14+ has built-in OTel support via the instrumentation.ts file:
// instrumentation.ts, in the project root (not src/)
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
// Only runs in Node.js runtime, not Edge
const { NodeSDK } = await import('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = await import('@opentelemetry/exporter-trace-otlp-http');
const { getNodeAutoInstrumentations } = await import('@opentelemetry/auto-instrumentations-node');
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
}
}// next.config.ts
const nextConfig = {
experimental: {
instrumentationHook: true, // enable instrumentation.ts
},
};Edge Runtime limitation: OTel's Node.js SDK does not run in Edge Functions. For Edge spans, use @vercel/otel which provides an Edge-compatible implementation.
#2Reading Traces to Find Bottlenecks
Once traces appear in Jaeger or Grafana, look for:
- Long-duration spans, the span that takes the most time is your bottleneck. Sort by duration.
- N+1 query patterns, 50 spans all named
db.querywith nearly identical attributes means your code runs 50 individual queries where one could do. - Sequential vs parallel, spans stacked vertically (sequential) vs overlapping (parallel). Move independent operations to
Promise.all(). - Span gaps, time between spans with no child span means synchronous CPU work or untraced I/O.
- Error spans, red spans or spans with
status.code = ERRORattributes immediately surface failures.
#2Common Mistakes
- Loading the SDK after your app code. The SDK must be loaded with
--requirebefore any other module, because it patches modules on import. If Express or your DB client loads first, it won't be instrumented. - Not calling
span.end(). Unclosed spans are never exported. Usetry/finallyto ensure spans always end. - High-cardinality attributes as span names.
span.name = userIdorspan.name = orderIdcreates millions of unique span names, which breaks most tracing backends. Use fixed span names and put variable data in span attributes. - Forgetting
sdk.shutdown()on process exit. The OTLP exporter batches spans and flushes periodically. Without a graceful shutdown, the last batch of spans may not be exported before the process exits. - Instrumenting Edge Functions with the Node.js SDK. It will fail silently or crash. Use the
NEXT_RUNTIME === 'nodejs'check or@vercel/otelfor Edge-compatible instrumentation.
#2Frequently Asked Questions
#3Is OpenTelemetry production-ready in 2026?
Yes. The tracing specification and the JavaScript SDK are stable. Metrics and logs APIs are also stable. OpenTelemetry is used by Google, Microsoft, AWS, and every major observability vendor. It is the industry standard.
#3Do I need a paid backend to use OTel?
No. Jaeger runs locally with zero setup (one docker run command). Grafana Cloud has a 50GB free tier. Honeycomb has a 20M events/month free tier. You can run a full production setup for low-traffic apps entirely for free.
#3How much overhead does OTel add?
Auto-instrumentation adds 5–15ms of cold-start overhead per module patched, and minimal per-request overhead (microseconds for span creation and attribute setting). The OTLP exporter batches and exports asynchronously, so it does not block your requests. The overhead is acceptable for all production workloads.
#3Can OTel trace across multiple services?
Yes, this is its primary purpose. OTel propagates a traceparent header through HTTP calls automatically. When service A calls service B, both spans appear in the same trace tree, showing the complete latency chain.
#3How do I correlate OTel traces with my logs?
Add the current trace ID and span ID to your log messages:
import { trace } from '@opentelemetry/api';
function log(message: string, data?: object) {
const span = trace.getActiveSpan();
const traceId = span?.spanContext().traceId;
const spanId = span?.spanContext().spanId;
console.log(JSON.stringify({ message, traceId, spanId, ...data }));
}Most observability platforms (Grafana Loki, Datadog) can then link logs to traces automatically using the trace ID.
Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.
#2Sources / Further reading
- OpenTelemetry - Official documentation
- OpenTelemetry - JavaScript SDK
- W3C - Trace Context specification
- OpenTelemetry - Semantic Conventions
#2Try These Tools on AllDevToolsHub
- REST API Tester — Test instrumented API endpoints and inspect trace headers in responses
- HTTP Header Checker — Inspect HTTP headers including traceparent and tracestate for distributed tracing
- cURL Command Generator — Generate cURL commands with trace context headers for testing instrumented services
All tools run entirely in your browser. No sign-up, no data upload, no server round-trips.
#2Try These Tools
Quick Summary
>- A hands-on guide to OpenTelemetry in Node.js and Next.js in 2026. Covers auto-instrumentation, manual spans for custom business logic, exporting traces to Grafana Cloud and Jaeger (free tier), reading traces to find bottlenecks, and the common pitfalls with server components and edge functions.
Tools Mentioned in This Article
.env File Parser
Parse, edit, and export .env files as JSON, Docker flags, or shell exports.
Crontab Expression Generator
Build and validate cron expressions with a visual editor.
Regex Tester
Test and debug regular expressions with live matches.
.gitignore Generator
Generate .gitignore files for any language, framework, OS, or editor.
Tools, tactics, and toughened-up tips, once a week
New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.
Found an error or have feedback?
We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.