OpenTelemetry skill for Claude Code
One request, one connected trace. Claude starts with zero-code instrumentation and a local Jaeger so output is visible immediately, then adds manual spans around business operations, propagates W3C context through HTTP and message queues, and links logs to traces. Telemetry goes through a Collector with memory limits, attribute scrubbing and parent-based sampling, verified with one end-to-end trace.
- name
opentelemetry - size244 lines · ≈2,600 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Instruments services with OpenTelemetry — SDK setup and auto-instrumentation for Node, Python, Java, and Go, manual spans with semantic conventions, metrics with bounded cardinality, trace-correlated logs, W3C context propagation across HTTP and message queues, sampling, and OpenTelemetry Collector pipelines. Use when adding tracing or metrics to a service, when traces break between services, when spans or metrics are missing in Jaeger, Tempo, Honeycomb, Datadog, or Grafana, or when configuring an otel collector. Try asking
- Add OpenTelemetry tracing to our Node and Python services and send it to Grafana Tempo
- Our traces stop at the Kafka consumer. Make the context carry through
- Write an otel collector config that scrubs PII and samples 10% of traces
Use it when
- A service has no tracing or metrics yet
- Traces break between services or across a queue
- Spans are missing or duplicated in your tracing backend
- You need a Collector pipeline for sampling, scrubbing, or routing
Skip it when
- You need tracing specific to LLM calls and prompts — use llm-observability instead
- You are searching existing logs for an incident — use log-analysis instead
How to check the result
A single request shows up as one trace with correctly nested spans from every service, its log lines carry the same trace_id, and metric series counts stay bounded.
The common mistake it prevents
Starting the SDK after importing HTTP or database libraries, which leaves them unpatched and produces no spans and no error message.
Instrumentation is done when one user request produces a single connected trace across
every service it touches, spans follow semantic conventions, metrics have bounded
label cardinality, logs carry trace_id, and telemetry flows through a Collector you
can reconfigure without redeploying apps.
Ground rules
- Start with auto-instrumentation; add manual spans only around business operations the auto layer cannot see.
- Initialize the SDK before any instrumented library is imported.
- Set
service.name,service.version, anddeployment.environment.nameon every service. - Send to a Collector (OTLP), not directly to vendors, so sampling, scrubbing, and routing live in one place.
- Never put PII, secrets, or unbounded values (user IDs, raw URLs with IDs) into metric attributes. Be careful with span attributes too.
- Flush on shutdown; short-lived processes and serverless lose buffered spans otherwise.
Workflow
- [ ] 1. Inventory services, languages, existing telemetry, backend
- [ ] 2. Stand up a Collector (or local Jaeger) to see output immediately
- [ ] 3. Enable auto-instrumentation with env config
- [ ] 4. Add manual spans/metrics for key business operations
- [ ] 5. Propagate context across async boundaries (queues, jobs)
- [ ] 6. Correlate logs with trace IDs
- [ ] 7. Configure sampling and processors; verify end-to-end traces
2. Local backend for verification
docker run --rm -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/jaeger:latest
# UI: http://localhost:16686 ; OTLP gRPC :4317, OTLP HTTP :4318
Pin the Jaeger tag in anything committed.
3. Auto-instrumentation
Standard env vars work across SDKs:
export OTEL_SERVICE_NAME=checkout
export OTEL_RESOURCE_ATTRIBUTES=service.version=1.14.2,deployment.environment.name=staging
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=1.0 # 100% in dev; lower in prod
| Language | Zero-code setup |
|---|---|
| Node.js | npm i @opentelemetry/api @opentelemetry/auto-instrumentations-node then node --require @opentelemetry/auto-instrumentations-node/register app.js |
| Python | pip install opentelemetry-distro opentelemetry-exporter-otlp && opentelemetry-bootstrap -a install then opentelemetry-instrument python app.py |
| Java | java -javaagent:opentelemetry-javaagent.jar -jar app.jar |
| .NET | OpenTelemetry .NET automatic instrumentation, or the OpenTelemetry.Extensions.Hosting packages |
| Go | No agent by default: SDK in code plus contrib wrappers (otelhttp, otelgrpc, otelsql) |
Node SDK in code (when you need custom config; SDK 2.x API):
// instrumentation.mjs — load first: node --import ./instrumentation.mjs app.mjs
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
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]: 'checkout',
[ATTR_SERVICE_VERSION]: process.env.APP_VERSION ?? 'dev',
}),
traceExporter: new OTLPTraceExporter(),
metricReader: new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() }),
instrumentations: [getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false }, // very noisy
})],
});
sdk.start();
process.on('SIGTERM', () => sdk.shutdown().finally(() => process.exit(0)));
ESM apps may need the instrumentation loader hook for some libraries to be patched;
check the current @opentelemetry/instrumentation docs for the ESM registration flag.
4. Manual spans and metrics
import { trace, metrics, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('checkout');
const meter = metrics.getMeter('checkout');
const ordersPlaced = meter.createCounter('orders.placed', { unit: '{order}' });
const paymentDuration = meter.createHistogram('payment.duration', { unit: 's' });
export async function placeOrder(cart) {
return tracer.startActiveSpan('placeOrder', async (span) => {
span.setAttribute('order.item_count', cart.items.length);
const start = performance.now();
try {
const result = await chargeCard(cart);
ordersPlaced.add(1, { 'payment.method': cart.method }); // bounded values only
return result;
} catch (err) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
throw err;
} finally {
paymentDuration.record((performance.now() - start) / 1000, { 'payment.method': cart.method });
span.end();
}
});
}
from opentelemetry import trace
tracer = trace.get_tracer("checkout")
def place_order(cart):
with tracer.start_as_current_span("place_order") as span:
span.set_attribute("order.item_count", len(cart.items))
return charge_card(cart) # exceptions are recorded and status set automatically
Span naming: low-cardinality operation names (GET /orders/{id}, placeOrder), never
GET /orders/12345. Put IDs in attributes, not names.
Instrument choice: counter for things that only go up; up-down counter for in-flight counts; histogram for durations and sizes (unit seconds); gauge (observable) for sampled values like queue depth.
5. Context propagation
- HTTP: auto-instrumentation injects/extracts W3C
traceparent/tracestate. If traces break at a hop, check that proxies and API gateways forward these headers. - Queues (Kafka, SQS, RabbitMQ): inject into message headers on produce, extract on consume. Use the library’s instrumentation when available; otherwise:
import { context, propagation, trace } from '@opentelemetry/api';
const headers = {};
propagation.inject(context.active(), headers); // producer: attach headers to message
const parent = propagation.extract(context.active(), msg.headers); // consumer
await context.with(parent, () => tracer.startActiveSpan('process order', async (s) => { /* ... */ s.end(); }));
- Batch consumers: create one span per batch with span links to each message’s context rather than pretending one parent.
6. Logs
Inject trace_id and span_id into structured logs (pino, winston, structlog, logback
have OTel integrations or MDC support), so a log line links to its trace. Ship logs via
the Collector (filelog receiver) or the SDK log bridge, not both.
7. Collector pipeline
# otel-collector.yaml (otelcol-contrib)
receivers:
otlp:
protocols:
grpc: { endpoint: 0.0.0.0:4317 }
http: { endpoint: 0.0.0.0:4318 }
processors:
memory_limiter: { check_interval: 1s, limit_percentage: 80, spike_limit_percentage: 20 }
attributes/scrub:
actions:
- { key: http.request.header.authorization, action: delete }
- { key: user.email, action: delete }
batch: {}
exporters:
otlphttp/backend:
endpoint: https://otlp.example-vendor.com
headers: { authorization: "Bearer ${env:OTLP_TOKEN}" }
debug: { verbosity: basic }
extensions:
health_check: {}
service:
extensions: [health_check]
pipelines:
traces: { receivers: [otlp], processors: [memory_limiter, attributes/scrub, batch], exporters: [otlphttp/backend] }
metrics: { receivers: [otlp], processors: [memory_limiter, batch], exporters: [otlphttp/backend] }
logs: { receivers: [otlp], processors: [memory_limiter, batch], exporters: [otlphttp/backend] }
memory_limiter goes first, batch last. Validate: otelcol-contrib validate --config otel-collector.yaml.
Sampling:
| Volume | Strategy |
|---|---|
| Low (< ~100 req/s total) | Keep 100% |
| Medium/high | Head sampling parentbased_traceidratio at 5–20% in SDKs |
| Need all errors and slow traces | Tail sampling processor in the Collector; route all spans of a trace to the same Collector (loadbalancing exporter by trace ID) |
Always use a parent-based sampler so downstream services honor the upstream decision.
Verification
- Send one request; in Jaeger/your backend, find a single trace containing spans from every service in the path, with correct parent-child nesting.
- Check span attributes use semantic convention names (
http.request.method,http.response.status_code,url.path,db.system.nameor the convention version your SDK emits). - Query metrics: series count per metric stays small (no IDs in attributes).
- Pick a log line from the request; its
trace_idopens the same trace. - Kill the process with SIGTERM during load; spans up to shutdown still arrive.
Gotchas
- Port/protocol mismatch: 4317 is gRPC, 4318 is HTTP. With the generic
OTEL_EXPORTER_OTLP_ENDPOINT, HTTP exporters append/v1/traces; with the signal-specificOTEL_EXPORTER_OTLP_TRACES_ENDPOINT, you must include the path. - Importing
express/pgbefore the SDK starts leaves them unpatched: no spans, no error. - Two instrumentation layers (vendor agent + OTel) produce duplicate spans.
- Semantic convention names changed across versions (
http.method->http.request.method); dashboards built on old names silently go empty after an SDK upgrade. - Lambda/serverless: call
forceFlush()before the handler returns, or use the OTel Lambda layer. fsand DNS instrumentation can generate thousands of spans per request; disable them.
Output
**Services instrumented:** checkout (Node, auto + 3 manual spans), payments (Python, auto)
**Resource attributes:** service.name, service.version, deployment.environment.name
**Propagation:** HTTP (W3C), Kafka headers on orders topic
**Collector:** memory_limiter -> scrub -> batch -> vendor OTLP; config validated
**Sampling:** parentbased_traceidratio 0.1 in prod; tail sampling keeps errors
**Proof:** trace 4bf92f3577b34da6a3ce929d0e0e4736 spans checkout -> payments -> Kafka consumer About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.