Node.js and NestJS
Send OpenTelemetry traces from Node.js and NestJS applications to KubeSense.
Node.js applications, including NestJS, Express, Fastify and Koa, are instrumented with the OpenTelemetry auto-instrumentations package. It creates spans for incoming requests, NestJS controllers and handlers, outgoing HTTP calls, databases (PostgreSQL, MySQL, MongoDB, Redis and more) and message queues, with no code changes.
Option 1: Kubernetes with the OpenTelemetry Operator (zero code)
If you have set up the OpenTelemetry Operator, add this annotation to the pod template and restart the workload:
spec:
template:
metadata:
annotations:
instrumentation.opentelemetry.io/inject-nodejs: "kubesense/otel-instrumentation"The operator injects the agent and the configuration. No package installation is needed.
Option 2: Auto-instrumentation on VMs (no code changes)
Use this on VMs, or on Kubernetes clusters without the operator. You only install packages and set environment variables; the application code is not changed.
Prerequisites
- Node.js 18 or later
- Network access from the application to the KubeSense trace endpoint on port
33443. See Instrumentation.
Step 1: Install the packages
npm install --save @opentelemetry/api @opentelemetry/auto-instrumentations-nodeStep 2: Configure the exporter with environment variables
| Variable | Value | Purpose |
|---|---|---|
OTEL_SERVICE_NAME | e.g. orders-api | Service name in KubeSense |
OTEL_EXPORTER_OTLP_ENDPOINT | http://<KUBESENSE_INGEST_HOST>:33443 | KubeSense trace endpoint (base URL, no path). Inside the KubeSense cluster, use http://kubesense-kubecol.kubesense.svc.cluster.local:33443 |
OTEL_EXPORTER_OTLP_PROTOCOL | http/protobuf | Port 33443 accepts OTLP over HTTP |
OTEL_RESOURCE_ATTRIBUTES | kubesense.cluster=<CLUSTER_NAME>,kubesense.env_type=legacy | Use kubesense.env_type=k8s when the app runs on Kubernetes |
OTEL_NODE_RESOURCE_DETECTORS | env,host,os,process | Adds host and process details to spans |
OTEL_METRICS_EXPORTER | none | Exports traces only |
OTEL_LOGS_EXPORTER | none | Exports traces only |
NODE_OPTIONS | --require @opentelemetry/auto-instrumentations-node/register | Loads the instrumentation before your application code |
Example:
export OTEL_SERVICE_NAME=orders-api
export OTEL_EXPORTER_OTLP_ENDPOINT=http://<KUBESENSE_INGEST_HOST>:33443
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_RESOURCE_ATTRIBUTES=kubesense.cluster=<CLUSTER_NAME>,kubesense.env_type=legacy
export OTEL_NODE_RESOURCE_DETECTORS=env,host,os,process
export OTEL_METRICS_EXPORTER=none
export OTEL_LOGS_EXPORTER=none
export NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register"
node dist/main.jsInstrumentation must load first: The instrumentation patches libraries when they are loaded, so it must start before your application code. NODE_OPTIONS (or node --require ...) handles this. Importing it from inside your code is too late for modules that are already loaded.
Step 3: Restart the application and verify
Generate some traffic, then open APM → Traces in KubeSense and filter by your service name.
NestJS
NestJS compiles to CommonJS by default, so the same --require setup works. NestJS-specific spans, such as Create Nest App and AppController.orders, come from the NestJS instrumentation included in the auto-instrumentations package.
NestJS version support: The NestJS instrumentation supports a specific range of NestJS major versions (4 to 11 at the time of writing). On a newer NestJS major version that it doesn't support yet, requests are still traced through the HTTP and Express spans, but controller and handler spans don't appear until the instrumentation adds support.
Start the compiled application with the instrumentation preloaded:
node --require @opentelemetry/auto-instrumentations-node/register dist/main.jsOr set it in package.json:
{
"scripts": {
"start:prod": "node --require @opentelemetry/auto-instrumentations-node/register dist/main.js"
}
}When you run with nest start (development), set NODE_OPTIONS instead, because nest start launches Node itself.
Container image (Kubernetes without the operator)
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist ./dist
ENV OTEL_SERVICE_NAME=orders-api \
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \
OTEL_NODE_RESOURCE_DETECTORS=env,host,os,process \
OTEL_METRICS_EXPORTER=none \
OTEL_LOGS_EXPORTER=none \
NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register"
CMD ["node", "dist/main.js"]Set OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_RESOURCE_ATTRIBUTES at deploy time in the Kubernetes manifest (see below), so the same image works in every environment.
Kubernetes without the operator
containers:
- name: app
image: <YOUR_REGISTRY>/orders-api:1.0.0
env:
- name: OTEL_SERVICE_NAME
value: orders-api
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://kubesense-kubecol.kubesense.svc.cluster.local:33443
- name: OTEL_EXPORTER_OTLP_PROTOCOL
value: http/protobuf
- name: OTEL_RESOURCE_ATTRIBUTES
value: kubesense.cluster=<CLUSTER_NAME>,kubesense.env_type=k8s
- name: OTEL_METRICS_EXPORTER
value: none
- name: OTEL_LOGS_EXPORTER
value: none
- name: NODE_OPTIONS
value: --require @opentelemetry/auto-instrumentations-node/registerReducing noise
Some instrumentations produce many low-value spans. Disable them with a comma-separated list:
OTEL_NODE_DISABLED_INSTRUMENTATIONS=fs,dns,netManual instrumentation (SDK)
With manual instrumentation, you install the OpenTelemetry SDK packages yourself and add the trace setup code to the application. Use this instead of Option 1 or Option 2, when you want to control the setup in code.
Step 1: Install the packages
npm install --save @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node| Package | Purpose |
|---|---|
@opentelemetry/sdk-node | OpenTelemetry SDK, including the OTLP trace exporter |
@opentelemetry/auto-instrumentations-node | Library instrumentations (HTTP, Express, NestJS, databases, ...) |
@opentelemetry/api | OpenTelemetry API |
Step 2: Add the setup code
The SDK creates the trace exporter itself from the OTEL_* environment variables (Step 3), so the setup code stays short.
JavaScript: create tracing.js:
// tracing.js: load before any other module (node --require ./tracing.js app.js)
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
// The trace exporter, endpoint, service name and resource attributes
// are all configured from the OTEL_* environment variables.
const sdk = new NodeSDK({
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0));
});Start the application with it preloaded:
node --require ./tracing.js app.jsTypeScript and NestJS: create src/tracing.ts, so that it is compiled into dist/ together with the rest of the application:
// src/tracing.ts: must be imported before any other module
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
// The trace exporter, endpoint, service name and resource attributes
// are all configured from the OTEL_* environment variables.
const sdk = new NodeSDK({
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0));
});Make it the first import in src/main.ts:
import './tracing'; // must be the first import
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';warning: In a TypeScript project, create src/tracing.ts, not tracing.js: the TypeScript compiler doesn't copy .js files into dist/. Also, don't set NODE_OPTIONS=--require @opentelemetry/auto-instrumentations-node/register together with this setup, because that starts a second SDK.
To add a library that ships its own instrumentation, such as Prisma, install it and add it to the instrumentations list, for example instrumentations: [getNodeAutoInstrumentations(), new PrismaInstrumentation()].
Step 3: Configure the endpoints
Set the environment variables from Manual SDK: where to send traces. They include OTEL_TRACES_EXPORTER=otlp and OTEL_LOGS_EXPORTER=none, which the Node.js SDK reads to decide what to export.
Capturing HTTP headers
By default, spans don't include HTTP request or response headers. The register entry point used in Option 2 has no environment variable to turn header capture on, so you add a small setup file that configures the HTTP instrumentation. The application code is still not changed.
Captured headers appear on HTTP spans as attributes named http.request.header.<name> and http.response.header.<name>, for example http.request.header.x-tenant-id.
There are two ways to do it. Choose one:
- Specific headers (recommended): you list the headers to capture in environment variables.
- All headers: every header is captured, except
authorization,cookie,set-cookieandproxy-authorization.
Sensitive data and span size: Headers can contain tokens, session IDs and personal data, and capturing every header makes each span larger. Capture only the headers you need where possible, and never add authorization or cookie to the list.
Step 1: Install the SDK package
npm install --save @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-nodeStep 2: Add the setup file
Create otel.js with one of the following.
Specific headers:
// otel.js: captures the HTTP headers listed in the OTEL_CAPTURE_* environment variables
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const list = (name) =>
(process.env[name] || '').split(',').map((h) => h.trim().toLowerCase()).filter(Boolean);
const sdk = new NodeSDK({
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': {
headersToSpanAttributes: {
server: {
requestHeaders: list('OTEL_CAPTURE_SERVER_REQUEST_HEADERS'),
responseHeaders: list('OTEL_CAPTURE_SERVER_RESPONSE_HEADERS'),
},
client: {
requestHeaders: list('OTEL_CAPTURE_CLIENT_REQUEST_HEADERS'),
responseHeaders: list('OTEL_CAPTURE_CLIENT_RESPONSE_HEADERS'),
},
},
},
}),
],
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0));
});All headers:
// otel.js: captures all HTTP request and response headers, except sensitive ones
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const SKIP = new Set(['authorization', 'cookie', 'set-cookie', 'proxy-authorization']);
const addHeaders = (span, prefix, headers) => {
for (const [name, value] of Object.entries(headers || {})) {
const key = name.toLowerCase();
if (!SKIP.has(key) && value !== undefined) {
span.setAttribute(`${prefix}.${key}`, Array.isArray(value) ? value.join(',') : String(value));
}
}
};
const sdk = new NodeSDK({
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': {
// Request headers: incoming requests (server) and outgoing calls (client)
requestHook: (span, req) =>
addHeaders(span, 'http.request.header', req.headers || (req.getHeaders && req.getHeaders())),
// Response headers: runs when the response ends, so headers set by the app are included
applyCustomAttributesOnSpan: (span, req, res) =>
addHeaders(span, 'http.response.header', res.headers || (res.getHeaders && res.getHeaders())),
},
}),
],
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0));
});Step 3: Update the environment variables
Keep the environment variables from Option 2, and change NODE_OPTIONS to load otel.js instead of the register entry point:
export NODE_OPTIONS="--require /path/to/otel.js"For specific headers, also list the headers to capture (comma-separated, case-insensitive):
| Variable | Captures |
|---|---|
OTEL_CAPTURE_SERVER_REQUEST_HEADERS | Headers of requests your application receives |
OTEL_CAPTURE_SERVER_RESPONSE_HEADERS | Headers of responses your application sends |
OTEL_CAPTURE_CLIENT_REQUEST_HEADERS | Headers of outgoing HTTP calls your application makes |
OTEL_CAPTURE_CLIENT_RESPONSE_HEADERS | Headers of responses to those outgoing calls |
export OTEL_CAPTURE_SERVER_REQUEST_HEADERS=user-agent,x-request-id,x-tenant-id
export OTEL_CAPTURE_SERVER_RESPONSE_HEADERS=content-type,x-response-idTo change the captured headers later, update these variables and restart the application.
note: Don't load both otel.js and @opentelemetry/auto-instrumentations-node/register, because that starts a second SDK. If you use manual instrumentation, pass the same '@opentelemetry/instrumentation-http' options to getNodeAutoInstrumentations() in your existing tracing.js or tracing.ts instead of adding otel.js.
Step 4: Restart and verify
Restart the application and send a request with a header you capture:
curl -H 'x-tenant-id: acme' http://<APP_HOST>:<PORT>/<PATH>Open the trace in APM → Traces. The server span shows attributes such as http.request.header.x-tenant-id: acme.
Troubleshooting
Set OTEL_LOG_LEVEL=debug temporarily to print the SDK's export activity to the console.
| Symptom | Check |
|---|---|
| No spans at all | The instrumentation is not loaded first. Check that NODE_OPTIONS or --require is applied to the process that runs your app |
ECONNREFUSED to port 33443 | Trace ingestion is not enabled on KubeSense, or the port is not reachable from the application |
| Exporter errors for metrics or logs | Set OTEL_METRICS_EXPORTER=none and OTEL_LOGS_EXPORTER=none |
| Duplicate spans | Both the register entry point and your own tracing.js are loaded |
ESM application ("type": "module") | --require works for CommonJS only. ES modules need the OpenTelemetry ESM loader. See the OpenTelemetry JavaScript documentation |