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

instrument-spans.mdreferences/

titleimpacttags
What Earns a SpanCRITICALinstrument, spans, judgment

What Earns a Span

Not every function deserves a span. Two questions decide it:

  1. Interesting? — does this work meaningfully move latency or failure for the request?
  2. Aggregable? — grouped by name and attributes, does it produce a useful trend?

Both yes → span. Anything else → an attribute on the span you already have.

The table

OperationInteresting?Aggregable?Span?
HTTP request handlerYes — variable latency, can failYes — by route, method, statusYes
Database queryYes — I/O bound, failure-proneYes — by query type, tableYes
External API callYes — network latency, someone else's uptimeYes — by endpoint, statusYes
Cache lookupYes — fast path vs slow pathYes — by cache name, hit/missYes
Queue publish / consumeYes — async boundary, where delay hidesYes — by queue, message typeYes
Business transactionYes — meaningful state changeYes — by type, outcomeYes
Background jobYes — runs unattended, fails silentlyYes — by job name, outcomeYes
Private helperNo — trivial CPU, predictableNo — too granularNo
Loop iterationMaybe, if slowNo — unbounded cardinalityNo
Getter / setterNo — no meaningful durationNo — nothing to group byNo
Pure-CPU validationNo — fast and predictableMaybeNo
Orchestration wrapperNo — only calls instrumented codeNo — duration is just the sum of childrenNo

The first block is mostly handled by auto-instrumentation. Your judgment is needed for the business operations underneath, which no SDK can recognise.

Three ways this goes wrong

Too many spans. A trace with thousands of 2ms spans is unreadable and almost never actionable. Roll them up: one span for the batch, with the detail as attributes on it. A loop that emits a span per iteration is the classic version of this.

Too few spans. A single opaque handler span covering 900ms of work tells you a request was slow and nothing else. Every gap in the waterfall is a place you'll have to guess.

Test spans left in. A span named test-span, debug, foo, or my-operation is an artefact of proving the pipeline worked. Delete it before finishing — validate-traces.md assertion A9 checks for exactly this.

Prefer attributes to child spans

A child span costs a JOIN to correlate with its parent's context. An attribute on the parent is immediately available in the same row, groupable alongside everything else on that request.

So instead of a child span for authentication, time it and record the duration on the span you already have. Instead of a child span per database call in a loop, count them and record the total. Both patterns — timing attributes and async request summaries — are in instrument-attributes.md, with code in references/custom-instrumentation.md.

Create the child span when you need its own failure status, its own attributes, or its position in the waterfall. Otherwise widen the parent.

Span names stay low-cardinality

The name is a grouping key. GET /api/users/{id} groups; GET /api/users/8823 does not — it creates a new group for every user and makes the data useless in aggregate.

IDs, emails, URLs with parameters, and timestamps belong in attributes, never in names. validate-traces.md assertion A6 checks the route pattern for this.

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