Instrument applications with the OpenTelemetry SDK and prove the telemetry is good by validating it against a local Kopai backend. Use when setting up observability, adding tracing/logging/metrics, deciding what to instrument or which attributes to add, retrofitting OTel into an existing codebase, threading context through call chains, configuring sampling, or when traces/logs/metrics aren't appearing after setup. Also use when users say things like "my traces aren't showing up", "I don't see any data", or "how do I add observability to my app". Do NOT use to investigate existing telemetry for a root cause (use root-cause-analysis), to build dashboards (use create-dashboard), or to instrument LLM and agent calls (use otel-genai-instrumentation).
—
—
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
| title | impact | tags |
|---|---|---|
| Sampling | MEDIUM | instrument, sampling, cost |
Start with no sampling. Add it when volume or cost forces the question — not before, and not by default. Sampling is a trade, and the thing you trade away is the ability to debug the rare event.
Running against a local Kopai backend, you almost never want sampling on: it hides data during exactly the phase where you're checking your instrumentation is correct. If your validation loop is missing spans, check the sampler before anything else.
An error affecting 0.1% of requests, head-sampled at 1%, is captured once per 100,000 occurrences. At moderate traffic that error effectively does not exist in your data — and it is precisely the kind you most need a trace for, because it's too rare to reproduce.
Sampling rates are a claim about which failures you're willing to never see. Make it deliberately.
The decision happens when the trace starts, before anything interesting has occurred.
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1 # keep 10%| Sampler | Behaviour |
|---|---|
always_on | Keep everything. The default, and the right start |
always_off | Keep nothing |
traceidratio | Keep a fixed fraction, decided at root |
parentbased_traceidratio | Respect the upstream decision; ratio only at the root |
Always use the parentbased_ variant in a multi-service system. Without it, each service
decides independently and you get traces with holes — a span here, its child dropped
there, and a waterfall that appears to show gaps in the work rather than gaps in the data.
Reach for it when: throughput is very high, traffic is homogeneous, and you can accept losing rare events.
The decision happens after the trace completes, so it can be made on what actually happened: keep every error, keep everything slow, keep a fraction of the routine traffic.
This needs a component that buffers whole traces — an OTel Collector with the
tail_sampling processor. That is infrastructure you now operate, and it must see every
span of a trace, which constrains how you load-balance.
Reach for it when: debuggability matters more than simplicity, which is most systems that page a human.
Whichever you choose, the rule is the same: sample the boring traffic, keep the interesting traffic. A 200 on a health check is worth ~0% sampling. A 500 on checkout is worth 100%, always.
Head sampling cannot express that distinction — it doesn't know the outcome yet. If your sampling policy needs to depend on what happened, you need tail sampling; there is no head-sampling configuration that gets you there.
parentbased_ fix above.After changing any sampler, re-drive and re-assert. With sampling on, validate-traces.md
assertions become probabilistic and stop being a reliable gate — so set
OTEL_TRACES_SAMPLER=always_on for the validation run, confirm the loop is green, and
only then apply the production sampling configuration.
references