CtrlK
BlogDocsLog inGet started
Tessl Logo

kopai/otel-instrumentation

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).

Quality

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

validate-traces.mdreferences/

titleimpacttags
Validate TracesCRITICALvalidate, traces, assertions

Validate Traces

Not "did some data show up" — a set of assertions that each pass or fail. The loop is green only when all of them pass against traffic you drove in drive-traffic.md during the same run.

Every command below filters on validation.run_id, so it reads only this run's spans.

Calibrate once

Kopai's JSON shape may differ across versions. Before asserting, look at one span so you know the real field names:

npx @kopai/cli traces search --resource-attr "validation.run_id=$RUN_ID" --limit 1 --json | jq .

The assertions below use the current CLI's row names — SpanName, TraceId, SpanId, ParentSpanId, SpanAttributes, ResourceAttributes, StatusCode. If the output uses different names, substitute them in every jq filter that follows. Do this once, at the top of the loop.

For assertions richer than jq can express comfortably, escalate to @kopai/sdk code mode — the typed query API, kq, and client.query() are documented in the root-cause-analysis skill. The CLI is the zero-install default.

A1 — Spans arrived for this run

npx @kopai/cli traces search --resource-attr "validation.run_id=$RUN_ID" --json \
  | jq 'length'

Pass: greater than zero. Fail: troubleshoot-no-data.md. Do not proceed — every assertion below depends on this.

A2 — The service identifies itself

npx @kopai/cli traces search --resource-attr "validation.run_id=$RUN_ID" --json \
  | jq -r '.. | objects | select(has("service.name")) | ."service.name"' | sort -u

Pass: exactly your OTEL_SERVICE_NAME, and nothing named unknown_service. Fail: the resource isn't configured — setup-environment.md.

A3 — Spans are connected, not orphans

An orphan is a span with no parent. Entry points are legitimately orphans; anything downstream of one is a context-propagation bug.

npx @kopai/cli traces search --resource-attr "validation.run_id=$RUN_ID" --json \
  | jq '[.[] | select((.ParentSpanId // "") == "")] | length as $orphans
        | "orphans: \($orphans)"'

Pass: orphans are only your entry-point spans — one per request or job you drove. Fail: if nearly every span is an orphan, context is not propagating — context-propagation.md. This is the most common silent failure in OTel: the code compiles and runs perfectly the whole time it is happening.

List the orphans by name to see which ones shouldn't be:

npx @kopai/cli traces search --resource-attr "validation.run_id=$RUN_ID" --json \
  | jq -r '.[] | select((.ParentSpanId // "") == "") | .SpanName' | sort | uniq -c | sort -rn

A database or HTTP-client span in that list is always a bug.

A4 — Traces have depth

npx @kopai/cli traces search --resource-attr "validation.run_id=$RUN_ID" --json \
  | jq -r 'group_by(.TraceId) | map(length)
           | "traces: \(length), spans per trace: min \(min) max \(max)"'

Pass: a typical request trace has more than one span — handler plus its I/O children. Fail: all single-span traces means auto-instrumentation isn't hooked into your database and HTTP clients, or context is being dropped. Check that the instrumentation package for each library is installed, then context-propagation.md.

A5 — Every route you drove shows up

npx @kopai/cli traces search --resource-attr "validation.run_id=$RUN_ID" --json \
  | jq -r '.[].SpanName' | sort -u

Pass: every entry point from your route sweep appears. Compare against the list you built in drive-traffic.md — this is exhaustive, not a spot check. Fail: a driven route with no span means middleware isn't wired for that path — troubleshoot-missing-spans.md.

A6 — Entry-point spans carry route context

npx @kopai/cli traces search --resource-attr "validation.run_id=$RUN_ID" \
  --span-attr "http.route=/api/users" --json | jq 'length'

Pass: non-zero, and http.route holds the route pattern (/api/users/{id}), not the concrete URL (/api/users/1). A concrete URL means unbounded cardinality — every distinct ID becomes its own group and aggregation stops working. Fail: framework middleware is missing, or installed after the router — context-propagation.md.

A7 — Errors are marked as errors

You drove deliberate failures, so this must return rows.

npx @kopai/cli traces search --resource-attr "validation.run_id=$RUN_ID" \
  --status-code ERROR --json | jq 'length'

Pass: roughly the number of failures you drove. Fail — zero rows: the error paths set no span status. The code caught the exception and returned a 500 while the span reported success. instrument-errors.md.

Then check those failures carry a slug:

npx @kopai/cli traces search --resource-attr "validation.run_id=$RUN_ID" \
  --status-code ERROR --json \
  | jq -r '.[] | [.SpanName, (.SpanAttributes["exception.slug"] // "NO-SLUG")] | @tsv'

Pass: no NO-SLUG rows — every failure site is greppable and groupable. Fail: each NO-SLUG is an error path nobody instrumented — instrument-errors.md.

A8 — The attribute inventory is wide enough

List every attribute key this run produced:

npx @kopai/cli traces search --resource-attr "validation.run_id=$RUN_ID" --json \
  | jq -r '.[].SpanAttributes // {} | keys[]' | sort -u

Diff that against references/attributes.md.

Pass: the run answers all three questions every investigation opens with — who was affected (user.id or your tenant equivalent), what changed (service.version or a deploy attribute), and where the time went (timing attributes or child spans on the slow paths). Missing any one of them means the next incident stalls. Fail: instrument-attributes.md, then re-drive.

A9 — No test spans left behind

npx @kopai/cli traces search --resource-attr "validation.run_id=$RUN_ID" --json \
  | jq -r '.[].SpanName' | grep -iE 'test|debug|foo|bar|tmp|xxx' || echo "clean"

Pass: clean. A span created only to prove tracing worked is an artefact — delete it before finishing.

Green

Record which assertions passed, and against which $RUN_ID. If you changed any instrumentation to fix a red assertion, mint a new $RUN_ID, re-drive, and re-run all assertions — a fix in one place routinely breaks another.

Then continue to validate-logs.md, validate-metrics.md, and validate-shutdown.md.

references

_sections.md

architectural-patterns.md

attributes.md

cli-reference.md

context-propagation.md

custom-instrumentation.md

drive-traffic.md

instrument-attributes.md

instrument-errors.md

instrument-spans.md

lang-cpp.md

lang-dotnet.md

lang-erlang.md

lang-fastify.md

lang-go.md

lang-java.md

lang-nextjs.md

lang-nodejs.md

lang-php.md

lang-python.md

lang-ruby.md

lang-rust.md

layered-telemetry.md

nextjs-examples.md

otel-docs.md

sampling.md

setup-backend.md

setup-environment.md

troubleshoot-missing-attrs.md

troubleshoot-missing-spans.md

troubleshoot-no-data.md

troubleshoot-wrong-port.md

validate-logs.md

validate-metrics.md

validate-shutdown.md

validate-traces.md

CHANGELOG.md

SKILL.md

tile.json