OpenTelemetry auto-instrumentation for Node.js Express apps doesn’t just passively record requests; it actively participates in the request lifecycle, injecting itself into the very fabric of how your application handles incoming traffic.

Let’s see it in action. Imagine a simple Express app:

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  // Simulate some work
  setTimeout(() => {
    res.send('Hello World!');
  }, 500);
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

Without any OpenTelemetry setup, this app just runs. Now, let’s add auto-instrumentation. You’ll need to install the necessary packages:

npm install --save @opentelemetry/api @opentelemetry/auto-instrumentations-node

Then, to run your application with instrumentation enabled, you use the node --require flag:

node --require '@opentelemetry/auto-instrumentations-node/register' index.js

When you send a request to http://localhost:3000/, OpenTelemetry’s auto-instrumentation for Express will kick in. It attaches itself to the http server and the Express router. For each incoming request, it creates a new trace. It records the start time, the HTTP method, the URL, and other relevant metadata. As the request progresses through your Express middleware and route handlers, the instrumentation captures events: entering a middleware, starting a route handler, finishing the response. When the response is sent back, the span for that request is ended, and its duration is calculated. This entire process happens automatically, without you having to manually add tracer.startSpan() or span.end() calls within your application code.

The problem auto-instrumentation solves is the boilerplate and potential for human error in manually instrumenting every single incoming HTTP request and outgoing HTTP client call. Manually adding instrumentation code to each route and each outbound HTTP request is tedious, error-prone, and often inconsistent. It also means that if you introduce a new route or a new outbound HTTP client call, you have to remember to instrument it. Auto-instrumentation ensures that all such operations are captured from the moment you enable it, providing a comprehensive view of your application’s behavior.

Internally, the @opentelemetry/auto-instrumentations-node package works by patching core Node.js modules and popular libraries. For Express, it specifically hooks into the http module’s request listener and the Express Router’s dispatch mechanism. When an HTTP request arrives, it intercepts the request event. It then creates an http.server span. As the request proceeds through Express’s middleware chain and eventually hits a route handler, it creates child spans for each of these stages. If your route handler makes an outbound HTTP request using a library like axios or the built-in http module, the @opentelemetry/auto-instrumentations-node package, which also includes instrumentation for http, will automatically create a child span for that outbound request, linking it back to the original incoming request’s trace. This creates a detailed, nested view of the entire request flow, from the initial incoming request all the way to any external services it might call.

The exact levers you control are primarily through environment variables and configuration passed to the register function if you choose a more explicit setup. For example, you can control which instrumentations are enabled. By default, it enables a set of common ones including Express, HTTP, and async_hooks. You can disable specific instrumentations or add new ones. You can also configure the exporter – where the trace data is sent. The default often sends to a local OpenTelemetry Collector or directly to a tracing backend like Jaeger or Zipkin. You might set OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 to direct your traces to a collector running on port 4318. The sampling rate is another critical lever, allowing you to reduce the volume of trace data by only recording a fraction of requests, which can be configured via environment variables like OTEL_TRACES_SAMPLER_ARG and OTEL_TRACES_SAMPLER.

The real surprise is how the async_hooks instrumentation, often enabled by default, provides the backbone for correctly associating asynchronous operations that happen after the initial request handler has conceptually finished but before the response is fully sent, like the setTimeout in our example. It uses async_hooks to track asynchronous resources and their parent context, ensuring that spans created within those asynchronous operations are correctly linked to the parent request span, even if they are initiated by timers or other event loop mechanisms. This is crucial for accurately measuring the total time spent processing a request, including any deferred work.

The next step is to explore how to configure custom attributes and events within your auto-instrumented spans to enrich your telemetry data.

Want structured learning?

Take the full Opentelemetry course →