Express OpenTelemetry instrumentation
This guide instruments an Express application with the OpenTelemetry Node.js SDK. The auto-instrumentations capture spans for every route and outbound HTTP or database call, and the OTLP exporters send traces, metrics, and logs to Osuite.
Prerequisites
Section titled “Prerequisites”- Node.js 18 or newer.
- An Express application you can start locally.
- An Osuite ingest token from Settings → API Keys.
Instrument the service
Section titled “Instrument the service”-
Install the OpenTelemetry packages
Terminal window npm install @opentelemetry/sdk-node \@opentelemetry/auto-instrumentations-node \@opentelemetry/exporter-trace-otlp-proto \@opentelemetry/exporter-metrics-otlp-proto \@opentelemetry/exporter-logs-otlp-proto -
Create the initialization module
Create
instrumentation.jsin your project root. This file is loaded before your application so the auto-instrumentations can patch libraries as they are required.instrumentation.js const { NodeSDK } = require('@opentelemetry/sdk-node');const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto');const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-proto');const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-proto');const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');const { BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');const sdk = new NodeSDK({traceExporter: new OTLPTraceExporter(),metricReader: new PeriodicExportingMetricReader({exporter: new OTLPMetricExporter(),}),logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())],instrumentations: [getNodeAutoInstrumentations()],});sdk.start();process.on('SIGTERM', () => {sdk.shutdown().finally(() => process.exit(0));});The exporters read the endpoint and token from environment variables, so this module contains no secrets and is identical across every service.
Configure and run
Section titled “Configure and run”The exporters and resource are configured entirely through environment variables:
OTEL_EXPORTER_OTLP_ENDPOINT— your Osuite ingest endpoint.OTEL_EXPORTER_OTLP_HEADERS— thex-osuite-ingest-tokenheader that authenticates ingestion.OTEL_SERVICE_NAME— the service name that identifies this service in Osuite.OTEL_RESOURCE_ATTRIBUTES— additional resource attributes such asservice.environment.
Load instrumentation.js before your app with node --require. Choose your deployment target:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.osuite.io:443"export OTEL_EXPORTER_OTLP_HEADERS="x-osuite-ingest-token=<your-ingest-token>"export OTEL_SERVICE_NAME="checkout-api"export OTEL_RESOURCE_ATTRIBUTES="service.environment=production"
node --require ./instrumentation.js server.js# DockerfileFROM node:22-slimWORKDIR /appCOPY package*.json ./RUN npm ci --omit=devCOPY . .CMD ["node", "--require", "./instrumentation.js", "server.js"]Pass the configuration at run time so the ingest token never lands in the image:
docker run \ -e OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.osuite.io:443" \ -e OTEL_EXPORTER_OTLP_HEADERS="x-osuite-ingest-token=<your-ingest-token>" \ -e OTEL_SERVICE_NAME="checkout-api" \ -e OTEL_RESOURCE_ATTRIBUTES="service.environment=production" \ checkout-apiStore the ingest token in a Secret:
kubectl create secret generic osuite-ingest \ --from-literal=otlp-headers="x-osuite-ingest-token=<your-ingest-token>"Reference it from the Deployment and keep instrumentation.js in the start command:
apiVersion: apps/v1kind: Deploymentmetadata: name: checkout-apispec: replicas: 1 selector: matchLabels: app: checkout-api template: metadata: labels: app: checkout-api spec: containers: - name: checkout-api image: checkout-api:latest command: ["node", "--require", "./instrumentation.js", "server.js"] env: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: "https://ingest.<region>.osuite.io:443" - name: OTEL_EXPORTER_OTLP_HEADERS valueFrom: secretKeyRef: name: osuite-ingest key: otlp-headers - name: OTEL_SERVICE_NAME value: "checkout-api" - name: OTEL_RESOURCE_ATTRIBUTES value: "service.environment=production"Framework notes
Section titled “Framework notes”Verify in Osuite
Section titled “Verify in Osuite”What you should see
Send a request to your API, then open APM. The service appears within a minute under the service.name you set, emitting request-rate and latency metrics. Opening a trace shows the inbound Express route span with child spans for any downstream HTTP or database calls.
Troubleshooting
Section titled “Troubleshooting”No data in Osuite after a few minutes? Work through these checks.
- Endpoint — confirm the exporter targets exactly
ingest.<region>.osuite.io:443for your region, over TLS. - Token — confirm the ingest header carries a valid
<your-ingest-token>and has not been rotated. - Pipeline — confirm the signal you expect (traces, logs, or metrics) is wired into an active pipeline with the
otlp/osuiteexporter attached. - Export errors — check the application or Collector logs for OTLP export failures (auth, DNS, TLS, connection refused).
- Network — confirm the host has outbound access to the ingest endpoint on port
443. - Timing — allow up to a minute for the first data to appear before assuming a failure.
Still stuck? Ask the Investigation Agent or contact support.
For Node-specific issues — ESM vs CommonJS loading, the --require flag, or missing spans — see Node.js troubleshooting.
Next steps
Section titled “Next steps”- Add custom spans and metrics for your business logic.
- Correlate application logs with traces.