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

attributes.mdreferences/

Attribute Catalog

The canonical list of attributes worth putting on spans, by category. Other rules point here rather than keeping their own lists.

Every attribute is a dimension you can group by, filter on, and correlate against every other attribute on the same row. The wider your events, the more questions you can answer without redeploying — which is the entire point, since the question you need at 3am is the one you didn't anticipate.

Verify anything here with:

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

Start with these

If you do nothing else, cover the three questions every investigation opens with. validate-traces.md assertion A8 checks for them.

QuestionMinimum
Who is affected?user.id, user.type
What changed?service.version
Where did the time go?timing attributes on slow paths, cache.*

User and business context

The most valuable attributes you can add, and the ones no SDK can infer.

AttributeExamplesDescription
user.id2147483647Primary user identifier
user.typefree, premium, enterpriseTier or segment
user.auth_methodtoken, jwt, sso-githubHow they authenticated
user.team.id5387Team or group
user.org.id278Organisation, for enterprise accounts
user.age_days0, 637Account age — separates new users from established

Why: one enterprise account can be 10% of revenue. Without these you can't tell a weird edge case from a revenue-critical path breaking for your largest customer. With them, "all the slow requests are one tenant" is one query.

Careful: user.id is usually fine; names, emails, and payment details usually aren't. Check your compliance regime before putting personal data in telemetry.


Service metadata

AttributeExamplesDescription
service.nameapi, checkoutThis service
service.environmentproduction, staging, localWhere it's running
service.teamweb-servicesOwning team — the on-call rotation to escalate to
service.slack_channel#web-servicesWhere to reach them

Why: during an incident the first question is often "who owns this?" These let you answer it from telemetry instead of a service catalogue nobody has updated.


Build and deploy

AttributeExamplesDescription
service.versionv123, 9731945Version string or image hash
service.build.git_hash6f6466b0e693Git SHA of the deployed commit
service.build.deployment.age_minutes1, 10230How long since this version deployed
service.build.deployment.triggermerge-to-mainWhat triggered the deploy
service.build.deployment.usersam@company.comWho shipped it

Why: "did something just get deployed?" is the most frequently asked question in incident response, and the answer is usually yes. service.version lets you compare error rates between versions directly, which turns a hunch into evidence in one query.

Set these from the environment so they can't drift:

export OTEL_RESOURCE_ATTRIBUTES="service.version=$(git rev-parse --short HEAD)"

Feature flags

AttributeExamplesDescription
feature_flag.<name>true, falseValue of that flag for this request

One attribute per flag — feature_flag.new_checkout, feature_flag.auth_v2.

Why: flags are how you test in production, but only if you can compare the two sides. Without them a partial rollout produces an error rate that moves for no visible reason.


Error details

AttributeExamplesDescription
errortrue, falseDid this fail
exception.slugerr-stripe-charge-failedStatic, greppable identifier for the throw site
exception.typeIOErrorProgrammatic exception type
exception.messageconnection resetMessage, for reading not grouping
exception.stacktraceStack trace where available
exception.expectedtrue, falseNormal operation (bot traffic, validation) vs a defect

Why the slug: it's low-cardinality so you can group by it, greppable so a chart leads straight to the line of code, and it detects its own gaps — any error = true with no slug is an error path nobody instrumented. See references/instrument-errors.md.


Timing breakdowns

AttributeExamplesDescription
auth.duration_ms52.2, 0.2Time in authentication
payload_parse.duration_ms22.1Time parsing the request
<operation>.duration_msAny sub-operation worth measuring

Why: a child span needs a JOIN to correlate with its parent's context. A duration on the parent sits in the same row as user tier, version, and route — so "slow requests spent their time in auth, for enterprise users, on the new version" is a single query.

Implementation: custom-instrumentation.md.


Async request summaries

AttributeExamplesDescription
stats.db_query_count7, 742Database queries this request issued
stats.db_query_duration_ms1254Cumulative time in the database
stats.http_requests_count1, 140Upstream calls made
stats.http_requests_duration_ms849Cumulative time in those calls
stats.cache_miss_count0, 31Cache misses

Why: a request making 742 database queries is a bug. Without a rollup it's invisible — it looks like one slow request, and the 742 child spans are only findable if you already suspected them.


Caching

AttributeExamplesDescription
cache.<name>true, falseDid this operation hit cache

One boolean per cacheable operation — cache.session, cache.feature_flags, cache.product_catalog.

Why: cache misses are among the most common causes of latency spikes, and a boolean per path makes "the slow ones all missed cache" fall out immediately.


HTTP, beyond auto-instrumentation

AttributeExamplesDescription
http.route/team/{team_id}/user/{user_id}The route pattern matched
http.route.param.<name>14739Extracted route parameter
http.route.query.<name>ascQuery parameters that matter
user_agent.devicecomputer, phoneDevice parsed from User-Agent
user_agent.browserChromeBrowser parsed from User-Agent

Why: without http.route a latency spike says "some requests are slow". With it, "only POST /checkout is slow". Parsed user-agent fields find client-specific bugs without regex over raw strings.

http.route must be the pattern, never the concrete URL — see validate-traces.md A6.


Infrastructure and orchestration

AttributeExamplesDescription
instance.id656993bd-40e1This instance of the service
instance.typem6i.xlargeInstance type
instance.memory_mb12336RAM available
instance.cpu_count4, 8Cores available
container.id / container.namea3bf90e006b2Container identity
k8s.cluster.name / k8s.pod.nameapi-clusterKubernetes topology
cloud.region / cloud.availability_zoneus-east-1cWhere it runs

Why: infrastructure problems present per-AZ or per-node. These are what let "all the slow requests are in us-east-1c" surface on its own instead of being guessed at.


Runtime versions

AttributeExamplesDescription
go.version / python.version / node.versiongo1.23.2Language runtime
<framework>.version7.2.1.1Web framework
<datastore>.version16.4Datastore

Why: "we upgraded the runtime recently — is that the memory increase?" is unanswerable without them.


Rate limits

AttributeExamplesDescription
ratelimit.limit200000Limit being enforced
ratelimit.remaining130000Budget left
ratelimit.used70000Consumed this window

Why: "why am I being rate limited?" is a common support ticket, and answering it otherwise means digging through a separate system.


Operational state

AttributeExamplesDescription
uptime_sec1533Seconds since start — exposes restarts
metrics.memory_mb153, 2593Memory at request time
metrics.cpu_load0.57, 5.89CPU load at request time
metrics.gc_count5390GC count
metrics.gc_pause_time_ms14, 325Time in GC

Why: "are the slow requests correlated with GC pauses?" becomes one query instead of two tools and a timestamp comparison.


Localization

AttributeExamplesDescription
localization.language_dirrtl, ltrText direction
localization.countrymexico, ukCountry the user associates with
localization.currencyUSD, CADPreferred currency

Why: bugs that only reproduce under RTL text or a specific currency are notoriously hard to find, and trivially filterable once these exist.


Never put on a span

  • Secrets, tokens, credentials, API keys
  • Full request or response bodies — extract the fields that matter
  • Personal data your compliance regime prohibits in telemetry
  • Anything you'd be unhappy to see in a screenshot pasted into a chat channel

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