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
Emit wide events, then prove they are good.
A span is a wide event: one flat record carrying the full context of a unit of work. Kopai runs on localhost, so you never ship instrumentation on faith — you drive traffic through the app yourself and assert on what arrived. The loop is green when every assertion in validate-traces passes. Nothing here is finished while an assertion is red.
| You are… | Start at |
|---|---|
| Setting up a service that has no telemetry | Workflow step 1 below |
| Retrofitting OTel into an existing codebase | context-propagation first — it is ~60% of the work — then the workflow |
| Chasing data that isn't arriving | troubleshoot-no-data, then step 4 of the workflow |
| # | Step | Done when |
|---|---|---|
| 1 | Start the backend — npx @kopai/app start | curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost:4318/v1/traces -H 'Content-Type: application/json' -d '{"resourceSpans":[]}' returns 2xx |
| 2 | Configure the environment — setup-environment | OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME, and a validation.run_id run tag are all exported in the shell that launches the app |
| 3 | Instrument — the matching lang-<language> rule under Language SDKs below, then the judgment rules | The app boots with the SDK loaded and prints no OTel export errors |
| 4 | Drive traffic yourself — drive-traffic | Every discovered entry point hit at least once and at least one deliberate failure driven |
| 5 | Assert — validate-traces, then validate-logs, validate-metrics, validate-shutdown | Every assertion passes. The loop is green |
| 6 | Fix — the matching troubleshoot-* rule under Fix it below | Return to step 4 and re-drive. Never stop on a red assertion |
Steps 4–6 are a loop, not a sequence. Report the instrumentation complete only when step 5 is green against traffic you drove in step 4 of the same run.
Two questions decide it:
Both yes → create a span. Otherwise → put an attribute on the span you already have.
When in doubt, prefer attributes over child spans. An attribute on the parent needs no JOIN, costs nothing extra to query, and is immediately groupable. Full decision table, plus the three ways this goes wrong, in instrument-spans.
One span, rich attributes, explicit error status. Node.js shown; every language follows the same shape — see your language's rule under Language SDKs below:
import { trace, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("checkout");
export async function processPayment(order: Order) {
return tracer.startActiveSpan("process-payment", async (span) => {
try {
span.setAttributes({
"order.id": order.id, // IDs are attributes, never span names
"order.total": order.total,
"payment.provider": order.provider,
});
const receipt = await charge(order);
span.setAttribute("payment.status", receipt.status);
return receipt;
} catch (err) {
span.recordException(err as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: (err as Error).message,
});
throw err;
} finally {
span.end();
}
});
}Full code for every pattern — nested spans, async fan-out, queues, manual context — in custom-instrumentation.
GET /api/users, db.query SELECT, process-paymentuser.id, order.total, cache.hithttp.route, db.system, rpc.serviceapp., checkout., <company>.Span names must be low-cardinality. Never interpolate an ID into a span name — that belongs in an attribute.
Detect the project's package manager first: the packageManager field in package.json
is authoritative when present; otherwise the lockfile decides — pnpm-lock.yaml →
pnpm, yarn.lock → yarn, bun.lock → bun, package-lock.json → npm. Run every
install with that manager — a second manager writes a second lockfile and diverges
silently. The npx @kopai/cli validation commands are manager-neutral; npx ships with
Node and never touches the project's dependency tree.
Check deprecation against the registry, not install output — only npm prints full
deprecation warnings at install time; pnpm truncates them and Yarn 4 prints none at
all. When npm does print them, read them: never filter an install through tail,
grep -v, or --quiet.
npm view @opentelemetry/instrumentation-<lib> deprecated # empty output = not deprecatednpm view is a pure registry read that works inside pnpm/yarn projects too (npm ships
with Node); native equivalents are pnpm view <pkg> deprecated and
yarn npm info <pkg> --fields deprecated --json (Yarn 4).
A deprecation message usually names its replacement — use it, never the deprecated
package; when none is named, follow the package's own migration guidance instead of
guessing. When a framework team ships its own plugin (Fastify → @fastify/otel), prefer
it over the contrib package: it registers through the framework's plugin system instead
of intercepting require/import — interception that native-ESM imports bypass unless
an experimental loader hook is added.
Task-scoped rule files. Load one only when the workflow points at it.
Setup — setup-backend, setup-environment
Language SDKs — lang-nodejs, lang-fastify, lang-nextjs, lang-python, lang-go, lang-java, lang-dotnet, lang-ruby, lang-php, lang-rust, lang-erlang, lang-cpp
What to instrument — instrument-spans (what earns a span),
instrument-attributes (which attributes, timing attributes, async summaries),
instrument-errors (error, span status, slug),
context-propagation (threading context; framework accessor gotchas),
layered-telemetry (traces vs metrics vs logs),
sampling
Prove it — drive-traffic (generate the traffic yourself), validate-traces, validate-logs, validate-metrics, validate-shutdown
Fix it — troubleshoot-no-data, troubleshoot-missing-spans, troubleshoot-missing-attrs, troubleshoot-wrong-port
Deep dives — load only when the task needs them:
references