mcpField
O
otel-node

otel-node

github.com/mizchiCloud & DevOps41644 installsUpdated 1w ago
Repository

About otel-node

A Node.js OpenTelemetry skill for instrumenting applications and diagnosing trace-export failures, especially with ESM bundles. It helps developers choose automatic or manual instrumentation and verify the collector pipeline.

Key features

  1. 01NodeSDK initialization and OTLP export
  2. 02HTTP, Express, PostgreSQL, and Redis instrumentation
  3. 03Manual spans and context propagation
  4. 04ESM bundling failure diagnosis and workarounds
  5. 05Collector pipeline verification

Use cases

  1. 01Adding traces to a Node.js service
  2. 02Troubleshooting missing spans in esbuild ESM output
  3. 03Instrumenting Hono with explicit middleware

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/mizchi/skills --skill otel-node

After installing, type/otel-nodein Claude Code to confirm it loaded.

Bundled filesREADME.md

Details

From the project README

OpenTelemetry — Node.js

SDK Initialization

// 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:

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:

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:

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:

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:

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

Comments

Markdown supported