Skip to main content
AllDevToolsHub
2026-07-14
Last reviewed: Jul 2026
DEVOPS
Est Read: 07_MIN

OpenTelemetry for JavaScript: Practical Observability in 2026

OpenTelemetry for JavaScript: Practical Observability in 2026
Processing_Node: 01

#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

bash
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

javascript
// 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

bash
# 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:

typescript
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)

bash
# .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)

bash
# 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)

bash
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:

typescript
// 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();
  }
}
javascript
// 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:

  1. Long-duration spans, the span that takes the most time is your bottleneck. Sort by duration.
  2. N+1 query patterns, 50 spans all named db.query with nearly identical attributes means your code runs 50 individual queries where one could do.
  3. Sequential vs parallel, spans stacked vertically (sequential) vs overlapping (parallel). Move independent operations to Promise.all().
  4. Span gaps, time between spans with no child span means synchronous CPU work or untraced I/O.
  5. Error spans, red spans or spans with status.code = ERROR attributes immediately surface failures.

#2Common Mistakes

  • Loading the SDK after your app code. The SDK must be loaded with --require before 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. Use try/finally to ensure spans always end.
  • High-cardinality attributes as span names. span.name = userId or span.name = orderId creates 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/otel for 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:

typescript
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

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

RJRahul JalavadiyaFounder & Lead Engineer
Published 2026-07-14Last reviewed 2026-07-14

Tools Mentioned in This Article

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.

Last reviewed: 2026-07-14
Security Memo
AT

Rahul Jalavadiya

Engineering Protocol V1

Specializing in local-first architecture and Zero-Trust developer workflows. No data leaves the machine.