Fastify OpenTelemetry instrumentation
This guide instruments a Fastify server with the OpenTelemetry Node.js SDK. The auto-instrumentations capture spans for every route, hook, and outbound call, and the OTLP exporters send traces, metrics, and logs to Osuite.
Prerequisites
Section titled “Prerequisites”- Node.js 18 or newer.
- Fastify 4 or newer.
- 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-protoThe Fastify instrumentation (
@opentelemetry/instrumentation-fastify) is included in the auto-instrumentations bundle, so there is nothing extra to add. -
Create the initialization module
Create
instrumentation.jsin your project root. It is loaded before your application so the auto-instrumentations can patch Fastify as it is 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));});
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="orders-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="orders-api" \ -e OTEL_RESOURCE_ATTRIBUTES="service.environment=production" \ orders-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: orders-apispec: replicas: 1 selector: matchLabels: app: orders-api template: metadata: labels: app: orders-api spec: containers: - name: orders-api image: orders-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: "orders-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 server, 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 Fastify request span with child spans for route hooks and any downstream 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.