Skip to content
Back to skills

Opentelemetry Instrumentation

ASecurity

Use when adding distributed tracing, debugging missing spans, fixing W3C traceparent propagation, configuring OTLP exporters (gRPC vs HTTP), choosing sampling strategies, setting resource attributes, or wiring auto-instrumentation libraries. Triggers: spans missing in Datadog/Honeycomb/Jaeger, "service.name = unknown_service", trace assembly broken across services, async work losing context, OTLP collector unreachable, sampling rate decisions, ESM vs CJS auto-instrumentation loader bugs. NOT ...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 24, 2026
devopsgobashnodenodejsexpressdebugginggitapidatabasebackend

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned September 24, 2026

npx -y skills add curiositech/port-daddy --skill opentelemetry-instrumentation --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Opentelemetry Instrumentation?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Opentelemetry Instrumentation
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/curiositech-opentelemetry-instrumentation-port-daddy/badge)](https://www.skillsdirectory.com/skills/curiositech-opentelemetry-instrumentation-port-daddy)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
license: Apache-2.0
allowed-tools: Read,Write,Edit,Bash,Glob,Grep,WebSearch,WebFetch
name: opentelemetry-instrumentation
description: 'Use when adding distributed tracing, debugging missing spans, fixing W3C traceparent propagation, configuring OTLP exporters (gRPC vs HTTP), choosing sampling strategies, setting resource attributes, or wiring auto-instrumentation libraries. Triggers: spans missing in Datadog/Honeycomb/Jaeger, "service.name = unknown_service", trace assembly broken across services, async work losing context, OTLP collector unreachable, sampling rate decisions, ESM vs CJS auto-instrumentation loader bugs. NOT for vendor-specific SDKs (Datadog APM, New Relic), structured-logging-only setups, or pre-OTel tracers (Jaeger client, Zipkin Brave).'
metadata:
  category: DevOps & Infrastructure
  tags:
    - opentelemetry
    - observability
    - tracing
    - metrics
    - otel
    - instrumentation
  provenance:
    kind: first-party
    owners: [port-daddy]
  pairs-with:
    - skill: logging-observability
      reason: Logs join traces only through the OTLP log pipeline; that skill owns the structured-logging side of the correlation
    - skill: observability-apm-expert
      reason: Owns the backend/APM side (dashboards, SLOs, alerting) that consumes the spans and metrics this skill emits
    - skill: monitoring-stack-deployer
      reason: Deploys the OTel Collector, storage, and Grafana stack that the OTLP exporters configured here point at
  io-contract:
    kind: deliverable
    consumes:
      - kind: instrumentation-requirement
        format: markdown
        description: What needs tracing -- services involved, runtimes (Node CJS/ESM, browser), async boundaries, vendor backend, cost constraints.
      - kind: otel-instrumentation-plan
        format: json
        description: A structured plan naming SDK load order, sampler, exporter, processor, and propagation choices, matching schemas/opentelemetry-instrumentation-plan.schema.json.
    produces:
      - kind: instrumentation-design
        format: markdown
        description: SDK setup, resource attributes, sampling, exporter, and context-propagation design with the rationale for each choice.
      - kind: otel-instrumentation-audit
        format: json
        description: A deterministic pass/fail audit of the otel-instrumentation-plan against this skill's Quality Gates, as produced by scripts/opentelemetry_instrumentation_audit.mjs.
---

# OpenTelemetry Instrumentation

OpenTelemetry is the vendor-neutral standard for traces, metrics, and logs. The SDK is split between *what gets recorded* (Tracer/Meter/Logger), *how it's sampled*, *how it's exported*, and *what context propagates*. Most pain comes from misaligning those four.

## When to use

- New service that needs distributed tracing.
- Existing service emits spans, but trace assembly breaks across the network boundary.
- Vendor backend shows "unknown_service" or trace IDs that don't link.
- You're paying too much for traces and need head/tail sampling.
- Async work (worker_threads, queues) drops the parent context.
- ESM-on-Node startup runs auto-instrumentation too late, missing http/express patches.

## Core capabilities

### Node SDK setup (auto-instrumentation)

```ts
// instrumentation.ts — must be loaded BEFORE any instrumented module.
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'orders-api',
    [ATTR_SERVICE_VERSION]: process.env.GIT_SHA ?? 'dev',
    'deployment.environment': process.env.NODE_ENV ?? 'dev',
  }),
  traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter(),
    exportIntervalMillis: 60_000,
  }),
  instrumentations: [getNodeAutoInstrumentations({
    // Disable auto-instrumentation for things you'll wrap manually.
    '@opentelemetry/instrumentation-fs': { enabled: false },
  })],
});
sdk.start();
```

Loading order:

```bash
# CJS — preload via -r
node -r ./instrumentation.js src/index.js

# ESM — use --import (Node 20.6+) or the dedicated register file
node --import @opentelemetry/auto-instrumentations-node/register src/index.mjs
```

Auto-instrumentation patches modules at `require`/`import` time. If your app imports `http` before the SDK starts, it's too late — those requests get no spans.

### Span lifecycle, manually

```ts
import { trace, context, SpanStatusCode } from '@opentelemetry/api';

const tracer = trace.getTracer('orders');

async function processOrder(order: Order) {
  return tracer.startActiveSpan('order.process', async (span) => {
    try {
      span.setAttribute('order.id', order.id);
      span.setAttribute('order.total_cents', order.totalCents);

      const result = await charge(order);
      span.setAttribute('order.charge_id', result.chargeId);

      span.setStatus({ code: SpanStatusCode.OK });
      return result;
    } catch (err) {
      span.recordException(err);
      span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
      throw err;
    } finally {
      span.end();
    }
  });
}
```

`startActiveSpan` sets the span as the current context for the duration of the callback. Anything started inside (DB calls, HTTP requests via instrumentation) becomes a child.

### Resource attributes — the ones that matter

Vendors use these for service grouping, environment filtering, deploy markers:

```
service.name              orders-api          # required
service.version           1.4.7-abc123        # for deploy markers
service.namespace         payments            # multi-tenant grouping
deployment.environment    production          # filter by env
host.name                 ${HOSTNAME}         # k8s pod name in DD/HC
process.runtime.name      nodejs              # auto-set
telemetry.sdk.language    nodejs              # auto-set
```

Skipping `service.name` is the #1 newbie mistake. Backend UIs collapse all your services into "unknown_service".

### Sampling

Three layers, head sampling first:

```ts
import { ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';

const sdk = new NodeSDK({
  sampler: new ParentBasedSampler({
    root: new TraceIdRatioBasedSampler(0.1), // sample 10% of root spans
    // Children inherit the parent's decision (default).
  }),
});
```

- **Head sampling** — decision made when the trace starts; cheap; can't see outcomes.
- **Tail sampling** — decision made by the OTel Collector after seeing the full trace; expensive (Collector buffers); can keep all errors.

Most teams: head-sample heavily (1-10%) but force keep error traces by passing a sampling-decision attribute up.

### Exporters

| Exporter | Protocol | Use when |
|----------|----------|----------|
| `OTLPTraceExporter` from `exporter-trace-otlp-grpc` | gRPC | Highest throughput; data-center-to-data-center. |
| `OTLPTraceExporter` from `exporter-trace-otlp-http` | HTTP/protobuf | Default; works through proxies/firewalls. |
| `OTLPTraceExporter` from `exporter-trace-otlp-json` | HTTP/JSON | Debugging; never use in prod (10x bigger payloads). |
| `ConsoleSpanExporter` | stdout | Local debugging. |

Wrap with `BatchSpanProcessor` (default in NodeSDK) so spans are batched and sent on a schedule. `SimpleSpanProcessor` sends synchronously — only for tests.

Set queue limits or risk a memory leak if the collector is down:

```ts
new BatchSpanProcessor(exporter, {
  maxQueueSize: 2048,
  maxExportBatchSize: 512,
  scheduledDelayMillis: 5000,
});
```

### Propagating context across async boundaries

Most cases auto-propagate via `context.with()`. Two cases that don't:

```ts
// 1. worker_threads — context doesn't cross.
const ctx = context.active();
worker.postMessage({ traceparent: serializeContext(ctx), payload });
// In the worker: context.with(deserializeContext(msg.traceparent), () => …)

// 2. Detached promises (fire-and-forget).
const detached = context.bind(context.active(), async () => {
  await sendNotification();   // gets the right parent span
});
detached();                   // not awaited
```

### Browser/client traces

```ts
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';

const provider = new WebTracerProvider();
provider.register({ contextManager: new ZoneContextManager() });

registerInstrumentations({
  instrumentations: [new FetchInstrumentation({
    propagateTraceHeaderCorsUrls: [/^https:\/\/api\.your-domain\.com/],
  })],
});
```

The CORS allowlist matters: `traceparent` is a custom header; without it, your backend allowlists, the browser blocks the request preflight.

### Metrics — pick the right instrument

```ts
import { metrics } from '@opentelemetry/api';
const meter = metrics.getMeter('orders');

const ordersTotal = meter.createCounter('orders.total');
const orderValue  = meter.createHistogram('order.value_cents', { unit: 'cent' });
const queueDepth  = meter.createObservableGauge('queue.depth');

ordersTotal.add(1, { region: 'us-west' });
orderValue.record(order.totalCents, { region: 'us-west' });
queueDepth.addCallback((obs) => obs.observe(getQueueDepth()));
```

Histograms beat averages — averages hide tail latency. Counters are monotonic; never use them for "current value" (use a Gauge).

## Anti-patterns

### Span context dropping in worker_threads

**Symptom:** Worker spans appear as new traces with no parent.
**Diagnosis:** OTel context is per-async-context; worker_threads is a fresh context.
**Fix:** Serialize the W3C traceparent across the message channel and re-establish on the other side using the W3C Trace Context propagator.

### Auto-instrumentation imported AFTER target modules

**Symptom:** http/express requests have no spans even though the SDK is loaded.
**Diagnosis:** Auto-instrumentation patches modules at require time. If the app imported `express` before SDK init, it's already monkey-patched-or-not for the rest of the process.
**Fix:** Use `--require` (CJS) or `--import` (ESM) to load instrumentation BEFORE the entry. Never `import './instrumentation.ts'` from `index.ts`.

### Missing `service.name`

**Symptom:** All traces show `unknown_service:node` in the vendor UI.
**Diagnosis:** Resource attributes weren't set; default fallback kicked in.
**Fix:** Always set `service.name` in `resourceFromAttributes`. Verify with the Console exporter locally before deploying.

### Sampling drift between services

**Symptom:** Traces are missing middle hops; you see only the entry and the database.
**Diagnosis:** Service A samples at 10% with no parent context; Service B receives the trace with `sampled=0` but its own sampler ignores parent and samples at 100%.
**Fix:** Use `ParentBasedSampler` on every service so they honor upstream decisions.

### Logging span IDs without joining via OTLP logs

**Symptom:** Logs say "trace_id=xxx" but the vendor UI doesn't link logs to traces.
**Diagnosis:** Most vendors auto-link only when logs come through the OTLP log pipeline (or via an SDK-native logger).
**Fix:** Switch to `@opentelemetry/api-logs` or use a logger integration that injects trace context AND ships via OTLP.

### Memory leak when collector unreachable

**Symptom:** Heap grows steadily; eventually OOM.
**Diagnosis:** BatchSpanProcessor buffer has no cap, or the cap is too high, and the collector has been down for hours.
**Fix:** Set `maxQueueSize`. Drop on overflow is the right behavior — better than crashing your service.

## Quality gates

- [ ] Every service sets `service.name`, `service.version`, `deployment.environment`.
- [ ] SDK init runs before any instrumented module imports (verified by a missing-span test).
- [ ] `ParentBasedSampler` configured on every service.
- [ ] Sampling rate documented in the runbook with per-environment values.
- [ ] OTLP endpoint health-checked at boot; clear error on failure.
- [ ] `BatchSpanProcessor.maxQueueSize` set to a finite value.
- [ ] Error paths call `span.recordException` and set `SpanStatusCode.ERROR`.
- [ ] Histograms used for latency/value; counters never used for current-state metrics.
- [ ] Browser fetch instrumentation has a `propagateTraceHeaderCorsUrls` allowlist.
- [ ] Trace context propagated across async boundaries (worker_threads, message queues).

## Deterministic Audit

Before rolling out (or reviewing) an instrumentation setup, write it as a JSON plan matching
`schemas/opentelemetry-instrumentation-plan.schema.json` and run it through the deterministic
auditor:

```bash
node scripts/opentelemetry_instrumentation_audit.mjs --input examples/sample-input.json
```

`auditOpentelemetryInstrumentation(plan)` (in `scripts/opentelemetry_instrumentation_audit.mjs`)
turns this skill's Anti-patterns and Quality Gates into machine-checkable rules over structured
fields — no keyword matching: a missing `service.name` (the unknown_service collapse), the SDK
loaded after target modules (spanless http/express), a non-`ParentBasedSampler` in a
multi-service deployment (sampling drift), the JSON or Console exporter in production, a
`SimpleSpanProcessor` outside tests, an uncapped batch queue (OOM when the collector is down),
counters used for latency, dropped context across worker_threads/queues, and browser fetch
instrumentation without a CORS propagation allowlist. It returns
`{ pass, score, findings, recommendations }` so a reviewer or CI gate can reject a
misconfiguration without re-deriving the reasoning. `examples/sample-input.json` is a
production Node service plan that audits `pass: true`. Version history lives in `CHANGELOG.md`.

## NOT for

- **Datadog APM, New Relic, AppDynamics agents** — vendor-native; different setup.
- **Structured-logging-only** observability — different layer. → `structured-logging-design` for the schema/redaction side; `grafana-dashboard-builder` for the visualization side.
- **Pre-OTel tracers** (Jaeger client, Zipkin Brave) — migration target should be OTel.
- **eBPF-based service maps** — different layer entirely.

Files in this skill

  • CHANGELOG.md819 B
  • SKILL.md14.4 KB
  • examples/sample-input.json594 B
  • schemas/opentelemetry-instrumentation-plan.schema.json3.6 KB
  • scripts/opentelemetry_instrumentation_audit.mjs9.8 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…