homeresume
 
   
🔍

Tracing Node.js Microservices with OpenTelemetry

June 30, 2023

Regarding microservices observability, tracing is important to catch bottlenecks of the services like slow requests and database queries.

OpenTelemetry is a set of monitoring tools that support integration with distributed tracing platforms like Jaeger, Zipkin, and New Relic. This post covers Jaeger v2 tracing setup for Node.js projects.

Prerequisites

  • Docker
  • Node.js version 26
  • OpenTelemetry packages:
npm i @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http @opentelemetry/resources \
@opentelemetry/sdk-trace-base @opentelemetry/semantic-conventions express

Jaeger v2

Start Jaeger in all-in-one mode with Docker Compose. Jaeger UI is at http://localhost:16686. OTLP HTTP receiver listens on port 4318.

services:
jaeger:
image: jaegertracing/jaeger:2.19.0
ports:
- 16686:16686
- 4317:4317
- 4318:4318

Run docker compose up -d from the demo folder (see below).

OpenTelemetry setup

Send traces to Jaeger over OTLP HTTP. Use resourceFromAttributes and semantic convention constants (ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION) to label the service. Auto-instrumentation picks up Express, HTTP clients, databases, and other supported libraries.

Process spans in batches with BatchSpanProcessor and shut the SDK down gracefully on SIGTERM.

import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
const traceExporter = new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces',
});
const sdk = new NodeSDK({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: `<service-name>-${process.env.NODE_ENV ?? 'dev'}`,
[ATTR_SERVICE_VERSION]: process.env.npm_package_version ?? '0.0.0',
[ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: process.env.NODE_ENV ?? 'dev',
}),
instrumentations: [getNodeAutoInstrumentations()],
spanProcessor: new BatchSpanProcessor(traceExporter),
});
sdk.start();
process.on('SIGTERM', () => {
sdk
.shutdown()
.then(() => console.log('Tracing terminated'))
.catch((error) => console.error('Error terminating tracing', error))
.finally(() => process.exit(0));
});

Import the tracing module before any other application code:

import './tracing.js';
// ...

Hit the app, then open Jaeger UI → SearchService and pick your service name (for example express-starter-dev in the demo).

Demo

Runnable code for this post lives in the jaeger-tracing-demo folder. Get access via code demos.