Observability for Node.js with OpenTelemetry

Khimananda Oli 7 min read Programming and Languages
Observability for Node.js with OpenTelemetry

By Khimananda Oli | Last reviewed: August 2026

Debugging latency spikes or silent failures in asynchronous Node.js applications is nearly impossible without correlated telemetry data. Observability for Node.js with OpenTelemetry solves this by unifying traces, metrics, and logs into a single vendor-neutral standard that works across any backend. This guide walks you through a production-grade implementation, moving beyond basic tutorials to cover auto-instrumentation, context propagation, and safe exporter configuration.

How does observability for Node.js with OpenTelemetry actually work?

At its core, OpenTelemetry (OTel) intercepts Node.js runtime primitives and popular library calls to generate telemetry signals automatically. When you initialize the SDK, it patches modules like http, express, pg, and ioredis at require-time. This means incoming HTTP requests automatically become trace spans, database queries are captured as child spans, and context propagates across async boundaries via the AsyncLocalStorage API.

Understanding this architecture prevents common mistakes. The SDK must be initialized before any other module loads; otherwise, patches fail silently. For teams familiar with the broader OpenTelemetry ecosystem, the Node.js implementation follows the same semantic conventions but relies heavily on the event loop's async context tracking. If your application uses worker threads or complex promise chains, verifying context propagation early saves hours of debugging missing spans later.

App Entry Pointrequire('sdk-node')OTel SDK CoreAsyncLocalStorageSpan ProcessorContext PropagationResource DetectionAuto-InstrumentationHTTP / Express / DBOTLP ExporterBackend / Collector
Observability for Node.js with OpenTelemetry initialization flow: SDK loads first, patches modules, processes spans, then exports via OTLP

How do you configure auto-instrumentation in Node.js?

Auto-instrumentation is where most teams should start. It provides immediate visibility with zero code changes to business logic. Create a separate tracing.ts (or .js) file that initializes the SDK before anything else imports.

// tracing.ts — MUST be imported before all other modules
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 { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: 'payment-service',
    [ATTR_SERVICE_VERSION]: '2.4.1',
    'deployment.environment': process.env.NODE_ENV || 'development',
  }),
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-express': { enabled: true },
      '@opentelemetry/instrumentation-pg': { enabled: true },
      '@opentelemetry/instrumentation-ioredis': { enabled: true },
      '@opentelemetry/instrumentation-http': { enabled: true },
    }),
  ],
});

sdk.start();

process.on('SIGTERM', () => {
  sdk.shutdown().then(
    () => console.log('OTel SDK shut down successfully'),
    (err) => console.error('Error shutting down OTel SDK', err)
  );
});

Then modify your start script to load this file first:

# package.json scripts
{
  "start": "node --require ./dist/tracing.js dist/server.js",
  "dev": "tsx --require ./src/tracing.ts src/server.ts"
}

A common mistake in 2026 is forgetting graceful shutdown. Without calling sdk.shutdown(), buffered spans are lost during deployments or restarts. Always register SIGTERM and SIGINT handlers, especially in containerized environments where pod termination windows are short.

When should you add manual instrumentation to Node.js services?

Auto-instrumentation covers infrastructure boundaries, but it cannot understand business semantics. You need manual spans when tracking multi-step workflows, external API calls not covered by plugins, or internal processing stages that matter for debugging. Refer to general instrumentation patterns for deeper context on span design.

import { trace, SpanStatusCode, context } from '@opentelemetry/api';

const tracer = trace.getTracer('payment-processing');

async function processPayment(orderId: string, amount: number) {
  return tracer.startActiveSpan('process-payment', async (span) => {
    span.setAttribute('order.id', orderId);
    span.setAttribute('payment.amount', amount);
    
    try {
      await validateOrder(orderId);
      
      // Create child span for gateway call
      const gatewayResult = await tracer.startActiveSpan(
        'call-payment-gateway',
        async (childSpan) => {
          const result = await paymentGateway.charge(amount);
          childSpan.setAttribute('gateway.transaction_id', result.txnId);
          childSpan.end();
          return result;
        }
      );
      
      span.setStatus({ code: SpanStatusCode.OK });
      return gatewayResult;
    } catch (error) {
      span.recordException(error);
      span.setStatus({ 
        code: SpanStatusCode.ERROR, 
        message: error.message 
      });
      throw error;
    } finally {
      span.end();
    }
  });
}

Key rules for manual instrumentation: always end spans (use try/finally), record exceptions explicitly, and avoid creating spans for trivial operations. High-cardinality attributes like user IDs or email addresses should be avoided unless you have confirmed your backend handles them efficiently. For teams building microservices, understanding how these manual spans connect across services is critical—see distributed tracing with OpenTelemetry and Jaeger for cross-service correlation strategies.

Telemetry Signal Needed?Infrastructure BoundaryHTTP, DB, Cache, QueueBusiness LogicWorkflow, Validation, TransformUse Auto-Instrumentation@opentelemetry/auto-instrumentations-nodeAdd Manual Spanstracer.startActiveSpan()Both feed same OTLP exporter → unified trace view
Decision framework for choosing auto vs manual instrumentation in observability for Node.js with OpenTelemetry

Which OpenTelemetry exporter and backend should you choose for Node.js?

The OTLP protocol is the universal standard in 2026, but your deployment target determines the optimal exporter configuration. Below is a practical comparison based on real production deployments across AWS, GCP, and self-hosted stacks.

BackendExporter PackageBest ForProduction Consideration
Grafana Tempo / Cloud@opentelemetry/exporter-trace-otlp-httpTeams already using Grafana stackUse HTTP/protobuf over gRPC for browser-compatible proxies
Jaeger@opentelemetry/exporter-trace-otlp-grpcPure tracing focus, CNCF-native shopsgRPC offers lower overhead; ensure collector supports v1.62+
AWS X-Ray / ADOT@opentelemetry/exporter-aws-xrayAWS-native teams needing service mapRequires AWS SDK credentials; limited attribute support
Datadog / New RelicVendor-specific OTLP endpointExisting vendor investmentVerify sampling compatibility; some vendors drop custom attrs
Local Dev / Debug@opentelemetry/exporter-trace-consoleDevelopment onlyNever enable in production; massive stdout overhead

In practice, I recommend deploying an OpenTelemetry Collector as a sidecar or DaemonSet rather than exporting directly from Node.js to backends. The Collector buffers during network blips, handles batching, and lets you swap backends without redeploying applications. For teams managing multiple services, this indirection pays for itself within weeks. See Prometheus and Grafana full monitoring stack for integrating OTel metrics alongside traces in the same observability pipeline.

What are common pitfalls when implementing observability for Node.js with OpenTelemetry?

After helping dozens of teams adopt OTel in production, these issues surface repeatedly:

  • SDK loaded too late: If your ORM or HTTP framework initializes before the SDK, auto-instrumentation fails silently. Always use --require or ESM loader hooks.
  • Missing context in async callbacks: Legacy callback-based APIs sometimes lose AsyncLocalStorage context. Wrap them with context.bind(context.active(), callback) or migrate to promises.
  • High-cardinality attributes: Adding user emails, request bodies, or UUIDs as span attributes explodes backend costs. Use span events or log correlation instead.
  • No sampling strategy: Default AlwaysOnSampler generates enormous volume in high-traffic services. Configure ParentBasedTraceIdRatio sampler with 0.1–0.01 ratio for production.
  • Ignoring log correlation: Traces without correlated logs force engineers to jump between tools. Inject trace_id and span_id into your structured logger (Pino/Winston) using the OTel log bridge or manual injection.

For teams serving Nepal-based users or operating under bandwidth constraints, aggressive sampling and local Collector buffering are non-negotiable. International backends add latency; batch exports every 5 seconds with max queue size limits to prevent memory pressure during outages.

❌ Common Pitfalls✅ Production FixesSDK initialized after app modules loadAuto-instrumentation patches miss targetsUse --require flag or ESM loader hookGuarantees SDK loads before all importsAlwaysOnSampler in high-traffic prodBackend cost explosion + export backlogParentBasedTraceIdRatio (0.01–0.1)Predictable volume + head-based consistencyDirect export to remote backendLost spans during network issuesLocal OTel Collector sidecar/DaemonSetBuffers + batches + backend agnosticHigh-cardinality attrs (user_id, email)Index bloat + query timeoutUse span events or log correlationKeep attrs low-cardinality + bounded
Pitfall-to-fix mapping for observability for Node.js with OpenTelemetry in production environments

Getting Started with Observability for Node.js with OpenTelemetry

Start with auto-instrumentation and a local Collector today. Get traces flowing end-to-end before adding manual spans or tuning sampling rates. Validate context propagation with a simple test endpoint that makes a DB call and returns the trace ID. Once your team trusts the data, expand to metrics and log correlation incrementally.

If your Node.js services handle sensitive data or operate under compliance requirements like SOC 2 or ISO 27001, ensure your OTel configuration excludes PII from attributes and encrypts export channels. Need help designing an audit-ready observability stack or troubleshooting a broken instrumentation setup? Reach out to discuss your specific architecture.

Frequently Asked Questions

OpenTelemetry is a vendor-neutral standard for collecting traces, metrics, and logs. For Node.js, it provides automatic instrumentation of HTTP, databases, and frameworks without code changes, enabling portable observability data export to any backend like Jaeger or Prometheus in 2026.

Install @opentelemetry/sdk-node and relevant auto-instrumentation packages via npm. Initialize the SDK before your application code runs using a separate instrumentation file registered with --require flag to ensure all modules are properly patched during startup.

Yes, typically under three percent CPU overhead with sampling enabled. Production deployments should configure head-based sampling at ten percent or less to minimize latency impact while retaining sufficient trace data for debugging distributed system issues effectively.

Yes, using @opentelemetry/instrumentation-express or instrumentation-fastify packages. These capture route names, middleware execution times, and error status codes automatically without modifying application code, providing detailed request-level visibility into your Node.js web framework performance.

Configure OTLPExporter with your Tempo endpoint URL and basic auth credentials. Set OTEL_EXPORTER_OTLP_ENDPOINT environment variable and use grpc or http protocol matching your Tempo deployment configuration for reliable trace ingestion in production environments.

Auto instrumentation patches libraries automatically at runtime with zero code changes. Manual instrumentation requires adding span creation code but offers custom attributes and business context. Combine both approaches for comprehensive coverage with domain-specific observability data in complex Node.js services.

Use attribute processors or custom span exporters to redact headers, query parameters, and payloads before export. Configure allowlists for safe attributes rather than blocklists to prevent accidental PII leakage in trace data stored by your observability backend.

Missing context propagation usually indicates async operations not wrapped by OpenTelemetry context manager. Ensure you use the latest SDK version and verify that promise chains, event emitters, and worker threads properly propagate context through Node.js async hooks.

Partially. ESM instrumentation requires experimental loader hooks and may not work with all packages. Check compatibility matrix before adopting. CJS remains more reliable for auto-instrumentation in 2026, though ESM support improves with each SDK release cycle.

Inject trace_id and span_id into log output using OpenTelemetry log correlation package. Configure Winston or Pino formatters to read active span context and include these identifiers, enabling direct navigation from log entries to corresponding traces in your backend.

Use parent-based trace ID ratio sampling at one to five percent for uniform distribution. Add tail-based sampling in your collector to retain all error traces and slow requests above threshold while dropping successful fast requests to control storage costs.

Yes, but cold starts complicate initialization. Use Lambda layers or container base images with pre-installed SDK. Configure shorter flush intervals and sync exporters to prevent trace loss during function freeze cycles in AWS Lambda or similar platforms.

Enable OTEL_LOG_LEVEL=debug and check console diagnostic logger output. Verify network connectivity, TLS certificates, and authentication tokens. Test with local collector first using docker-compose before troubleshooting remote endpoint connectivity issues in staging or production.

No, running both causes conflicts and duplicate instrumentation. Migrate fully to OpenTelemetry using vendor-specific exporters if staying with proprietary backends, or adopt OTLP-native backends. Remove legacy agent packages completely to avoid memory leaks and tracing corruption.

Node.js 18 LTS or newer.