Skip to main content

otel-node

Node.js OpenTelemetry setup — SDK init, auto-instrumentation packages, and the esbuild ESM silent-failure gotcha (instrumentation-* packages produce no spans when bundled with esbuild --format=esm). Use when adding OTel to a Node.js/Hono/Express service or debugging missing spans after bundling. See opentelemetry for signal design decisions.

Quellinformationen

Repository
mizchi/skills
Letzte Quellaktivität
16. September 2026 um 17:56
Erkannte Sprache von SKILL.md
Englisch
Sterne
364
Forks
6

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
2 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
otel-node
description
Node.js OpenTelemetry setup — SDK init, auto-instrumentation packages, and the esbuild ESM silent-failure gotcha (instrumentation-* packages produce no spans when bundled with esbuild --format=esm). Use when adding OTel to a Node.js/Hono/Express service or debugging missing spans after bundling. See opentelemetry for signal design decisions.
# OpenTelemetry — Node.js ## SDK Initialization ```typescript // otel.ts — import before everything else import { NodeSDK } from "@opentelemetry/sdk-node"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; import { Resource } from "@opentelemetry/resources"; import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; const exporter = new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + "/v1/traces", }); export const sdk = new NodeSDK({ resource: new Resource({ [SEMRESATTRS_SERVICE_NAME]: process.env.SERVICE_NAME ?? "my-service", }), spanProcessor: new BatchSpanProcessor(exporter), }); sdk.start(); process.on("SIGTERM", () => sdk.shutdown()); ``` Load before the app entry: `node --import ./otel.js server.js` or `tsx --import ./otel.ts server.ts`. ## Auto-instrumentation packages Add only what you need — each package patches a specific module: ```bash pnpm add @opentelemetry/instrumentation-http # node:http / node:https pnpm add @opentelemetry/instrumentation-express # Express routing spans pnpm add @opentelemetry/instrumentation-pg # postgres queries pnpm add @opentelemetry/instrumentation-ioredis # Redis commands ``` Register in SDK init: ```typescript import { HttpInstrumentation } from "@opentelemetry/instrumentation-http"; import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express"; export const sdk = new NodeSDK({ instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation()], // ... }); ``` **Hono**: no official instrumentation package. Use manual middleware (see below). ## esbuild ESM — Silent Auto-Instrumentation Failure ### Symptom App starts normally, SDK init log appears, but **no spans arrive at the collector**. No errors. Happens when: - Output format is `--format=esm` - Using `@opentelemetry/instrumentation-*` auto-instrumentation - Bundler is esbuild (also Vite / SWC — same root cause) ### Root cause `@opentelemetry/instrumentation-*` uses `require-in-the-middle` to hook `require()` and monkey-patch target modules (`node:http` etc.). In an esbuild ESM bundle, `import` is resolved statically at bundle time — `require()` is never called at runtime → hook never fires → no patches → no spans. No error is raised (silent failure). ### Fix A — Manual spans (recommended for Hono/esbuild) Write a middleware that creates spans explicitly: ```typescript import { context, propagation, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api"; const tracer = trace.getTracer("app"); app.use(async (c, next) => { const ctx = propagation.extract(context.active(), c.req.raw.headers); const route = new URL(c.req.url).pathname; await tracer.startActiveSpan( `${c.req.method} ${route}`, { kind: SpanKind.SERVER, attributes: { "http.method": c.req.method, "http.route": route } }, ctx, async (span) => { try { await next(); span.setAttribute("http.status_code", c.res.status); span.setStatus({ code: SpanStatusCode.OK }); } catch (e: any) { span.recordException(e); span.setStatus({ code: SpanStatusCode.ERROR, message: e?.message }); throw e; } finally { span.end(); } } ); }); ``` Propagate context to outgoing gRPC calls via metadata: ```typescript const callUnary = (method, req) => { const metadata = new grpc.Metadata(); propagation.inject(context.active(), metadata, { set: (carrier, key, value) => (carrier as grpc.Metadata).set(key, value), }); return new Promise((resolve, reject) => client[method](req, metadata, (err, res) => (err ? reject(err) : resolve(res))) ); }; ``` ### Fix B — Switch to CJS output Change esbuild to `--format=cjs`. `require()` hook fires normally. Works when no ESM-only dependencies are present. Hono supports CJS. ### Fix C — Run unbundled Use `tsx` or `node --loader ts-node/esm` + `--import @opentelemetry/auto-instrumentations-node/register`. Skip bundling for the Node process. Increases container image size; not recommended for production. ## Verifying the Pipeline Add a debug exporter to the OTel Collector temporarily: ```yaml exporters: debug: verbosity: detailed service: pipelines: traces: receivers: [otlp] exporters: [otlphttp/tempo, debug] ``` Collector logs show `Trace ID: ... Name: GET /api/users` when spans arrive. No output = broken before the collector. ## Related - `opentelemetry` — signal design, span naming, sampling, W3C propagation - `cloudflare-workers-otel-utels` — Cloudflare Workers telemetry (no Node runtime, fetch-boundary approach) - esbuild + Vite + SWC all share the same `require-in-the-middle` failure mode - `instrumentation-*` packages do not support static import patching as of 2026-05
Auf GitHub ansehen