pareelamre

otel-instrument-feature

"Instrument any new feature, endpoint, route, handler, worker, job, CLI command, or user-facing flow with OpenTelemetry spans, metrics, and logs before it lands. Triggers on requests like 'add a new endpoint', 'implement X', 'create a handler for Y', 'build a job to Z', 'add this feature', 'wire up a new route', or any code change that introduces a new business operation."

pareelamre 0 Updated 3mo ago
GitHub

Install

npx skillscat add pareelamre/analyzing-llm-rationale/otel-instrument-feature

Install via the SkillsCat registry.

SKILL.md

Instrument a new feature with OpenTelemetry

Use this skill whenever you are about to write — or have just finished writing — a new business operation in the user's project. A "new feature" is anything a human operator would want to find later when something looks wrong: a new HTTP route, RPC handler, GraphQL resolver, background job, queue consumer, cron task, CLI subcommand, agent tool, scheduled workflow, or any module-level function that represents a meaningful step in a user-facing flow.

Adding code that is just plumbing — a pure helper, a type, a config — does not trigger this skill. Adding code that takes a request in, does work, and produces a side effect or response does.

Step 0 — Confirm OTel is already wired

Before instrumenting, look for an existing bootstrap. Common signs: a telemetry.ts / observability.ts / instrumentation.ts / init_observability() module, @vercel/otel registerOTel(...), NodeSDK/sdk-node setup, Python LoggerProvider + OTLPLogExporter, superlogHeaders(...), or an inlined https://intake.superlog.sh endpoint with an sl_public_ token.

If you cannot find a bootstrap, do not invent one inside the new feature file. Tell the user the project still needs OTel wiring and that this is the job of the superlog-onboard skill — then continue with the feature implementation, leaving a single brief TODO at the bootstrap site (not on every span/metric/log call site). Do not block on this.

If a bootstrap exists, read the applicable companion skill before writing code:

  • otel-onboarding-style — general OTel taste and attribute conventions.
  • otel-python-style — Python.
  • otel-fastapi-style — FastAPI.
  • otel-livekit-style — LiveKit agents.
  • otel-nextjs-style — Next.js / Vercel.
  • otel-expo-style — Expo / React Native.
  • otel-supabase-edge-style — Supabase Edge Functions.
  • otel-generic-style — anything else (Go, Java, Ruby, Rust, .NET, PHP, Elixir, plain Node, …).

Match the local convention. If the file you are editing already uses withSpan from @superlog/otel-helpers, keep using it; if it uses tracer.startActiveSpan directly, keep using that. Do not introduce a second style alongside the existing one.

Step 1 — Decide what the new feature needs

For each new operation, walk this checklist explicitly before you write the instrumentation. The answer to each question may be "no, and here's why" — that is fine, but answer it.

Span — almost always yes

Wrap the operation in a single span whose name is domain.verb (order.submit, interview.create, email.send, job.reconcile, agent.run, voice.session, llm.generate_copy). The span lives at the entry point of the feature, not inside helpers.

Skip a custom span only when the operation is entirely covered by an existing auto-instrumentation (HTTP server entry, DB query, framework lifecycle) and adds no business-meaningful work on top. If in doubt, add the span — auto-instrumentation almost never has the right name or attributes for a domain operation.

Attributes to set on the span (only what is available, no PII):

  • Entity IDs the operator searches by: tenant.id, workspace.id, org.id, user.id, order.id, job.id, request.id. Use bounded IDs only — never raw email/phone/full request bodies.
  • Bounded categorical dimensions: outcome (success/failure/skipped), channel, kind, route, action.
  • Counts and sizes where they help explain latency: batch.size, items.count, payload.bytes.
  • For LLM call sites that provider instrumentation does not already cover: gen_ai.provider.name, gen_ai.request.model, app.gen_ai.use_case, app.gen_ai.call_site. Do not duplicate attributes OpenInference/provider instrumentation already captures.

On the error path, record the exception and set the span status to error. Then re-raise — do not swallow.

Metric — yes if this is a meaningful state transition or has latency that matters

Three categories. Decide which apply to the new feature:

  • Business counter — every meaningful outcome: orders.submitted, jobs.completed, emails.delivered, interviews.started, payments.failed. Tag with low-cardinality dimensions (tenant.id, outcome, channel). Never tag with raw user/order IDs.
  • Performance histogram — duration of the operation if a human would care about its tail. Reuse the span's existing timing rather than measuring twice when the SDK exposes it; otherwise record from time.perf_counter / performance.now() around the same boundary the span wraps.
  • Cost — for LLM call sites only, and only when provider instrumentation does not already capture token usage. Use llm.tokens.input / llm.tokens.output counters with unit="tokens". Do not add llm.cost_usd or any app-side pricing metric — Superlog computes cost centrally from provider/model/token data.

Acquire the meter and create instruments at module scope. Never create a meter or instrument inside the hot path.

Log — yes if a human would want a narrative trail

Use logs for narrative ("starting batch reconcile", "retry 2 of 3 after 5xx") and for exceptional events that need attention. Use the project's existing logger — do not introduce a new logging library and do not wrap log calls in a helper.

An error-level log must mean the operation cannot recover without manual intervention. Recoverable conditions go at warn or below.

Logs emitted inside the span automatically pick up trace_id / span_id if the bootstrap's log bridge is wired correctly. Do not start a throwaway span around a log call to force correlation.

Attributes vs metric dimensions

Span attributes can be high-cardinality (specific IDs, counts). Metric dimensions cannot — keep them bounded. If you find yourself wanting to tag a metric with order.id or request.id, that data belongs on the span, not the counter.

Step 2 — Write the instrumentation alongside the feature

Add the span at the entry point of the new operation. Do not sprinkle micro-spans across every helper inside it — one well-named span with rich attributes beats five generic ones. Sub-spans are useful only when there is a real sub-operation a human would want to isolate (an external API call, a DB transaction with non-obvious latency, a distinct phase like parse / validate / dispatch).

Add counters at the success and failure boundaries of the operation, with outcome tagged. Add a histogram where latency tail matters. Add logs at the start/end of long-running narratives and on exceptional branches.

Do not add backwards-compatibility shims, feature flags, or "telemetry on/off" toggles around individual call sites. The SDK already has a no-op path when no provider is registered; that is the only switch needed.

Do not invent helper APIs like recordCounter, sendSuperlogSpan, withTelemetry, trackEvent. Use the native OTel APIs (or the published withSpan from @superlog/otel-helpers in TS/JS, where the local style already uses it).

Step 3 — Verify the new feature emits what you claimed

Run the feature end-to-end through the project's normal entry point — hit the route with curl, invoke the CLI subcommand, enqueue the job, trigger the agent tool. A unit test that mocks the tracer does not count; the goal is to see the real OTLP payload leave the process.

If the dev server logs OTLP exporter activity (or the project has a debug exporter), confirm:

  • The span you added shows up with the name and attributes you claimed.
  • Counters you added incremented at least once.
  • A log line emitted inside the operation arrived with trace_id / span_id populated.

If the bootstrap uses a generated sl_public_ token and signup is not finished yet, ingest will return 401/403 — that is expected. The signal you care about here is "the SDK produced a payload and tried to send it", not "ingest accepted it."

If only spans appear and metrics/logs do not, the bootstrap has a wiring gap. That is a superlog-onboard problem, not this skill's — flag it to the user and continue with the feature work.

Hard rules

  • Never put PII in span attributes, metric dimensions, or log fields: no emails, phone numbers, passwords, tokens, full request bodies, plaintext message content.
  • Never tag metrics with raw user/order/request IDs. Those go on spans.
  • Never invent a wrapper API for telemetry; use the native OTel SDK and the local style's conventions.
  • Never start a throwaway span solely to make a log line carry trace_id. Fix the bridge instead, or accept the uncorrelated log.
  • Never add LLM cost math or pricing tables at call sites. Superlog computes cost centrally.
  • Do not block the feature on a missing bootstrap — leave a single TODO and tell the user to run superlog-onboard.

Categories