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

layered-telemetry.mdreferences/

titleimpacttags
Layered TelemetryMEDIUMinstrument, signals, traces, metrics, logs

Layered Telemetry

OpenTelemetry is trace-first: context propagation is the glue that correlates every signal. But traces, metrics, and logs each answer a different question, and emitting all three for the same event is layering, not duplication — each is a different view at a different level of detail.

Which signal

Three questions:

  1. Needs causality and full request context?Traces. What happened, in what order, with everything known about that specific request attached.
  2. Needs cheap long-term storage and fast alerting?Metrics. Pre-aggregated, so they stay cheap for months and evaluate fast — at the cost of losing the individual request.
  3. Rare, or required for audit?Logs. Things too infrequent to aggregate, or that must be retained verbatim.
Traces (spans)MetricsLogs
CapturesFull request contextNumbers with low-cardinality dimensionsText or structured fields
DiscardsNothingIndividual requests, high-cardinality contextCross-request correlation, unless trace-linked
Query powerGroup and filter on any dimensionFast aggregates on pre-chosen dimensionsSearch, structured filters
Cost scalingLinear with volumeExplodes with dimension cardinalityLinear with volume
Best atInvestigation, root causeAlerting, trendsAudit, rare events

The default answer is traces. The same instrumentation effort that produces a metric or a log line can produce a span — and the span can be counted like a metric, read like a log, and sliced by dimensions neither can offer.

Reach for metrics when

  • Alerting. Evaluating a threshold over a counter is cheaper and faster than aggregating spans, and it keeps working when traces are sampled.
  • Long horizons. Capacity trends over months, where per-request detail is dead weight.
  • Things that aren't requests. Queue depth, pool utilisation, memory, connection counts — state that exists between requests and belongs to no single one.

Keep metric dimensions low-cardinality. A user.id label on a counter creates a series per user; the same field on a span is free. validate-metrics.md assertion M4 checks this.

Reach for logs when

  • The event is too rare to aggregate — a config reload, a failover, a migration running
  • An audit requirement demands the verbatim record
  • The context is a startup or shutdown path where no span is active

Route them through OTel so each record carries trace context. An uncorrelated log is a line you'll read on its own with no idea what request produced it. validate-logs.md assertion L2 checks the correlation.

Histograms alongside spans

For a high-throughput HTTP service, emit both a span and a latency histogram per request.

The span gives full context and can be sampled hard for cost. The histogram is unsampled, cheap, and exact — so alerting stays trustworthy even at 1% trace sampling. When the histogram fires, the spans are there for the requests that survived sampling.

This is the main case where deliberately emitting the same event twice is correct.

Don't layer everything

Emitting a metric, a log, and a span for every operation triples the cost and gives you three places to check that disagree with each other. Layer where the signals do different jobs — usually alerting (metric) plus investigation (span) on your critical paths — and let the span carry everything else on its own.

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