Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when creating tracing spans or choosing span attribute keys in instrumentation code: name spans with the low-cardinality route template (GET /users/{id}, never the concrete URL) and record attributes under the OpenTelemetry registry keys (http.request.method, http.response.status_code, url.path, server.address, db.system.name, db.operation.name, error.type), lowercase and dot-namespaced, with custom attributes in their own namespace. Do NOT use for metric or log naming, choosing a tracing backend, sampling configuration, or debugging context propagation.
.claude/skills/otel-span-naming/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 12 |
| gemini-3.1-pro-preview | 100% | 3 |
| Model | Lift | Δ tokens | Δ turns | Cases | Verified |
|---|---|---|---|---|---|
| gemini-3.6-flashbest | +22% | +45% | 0% | 23 | 54d ago |
| gemini-3.5-flash | pending re-run | — | |||
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | — | — |
| case-05 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-17 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
Enforces OpenTelemetry semantic-convention names on every span and attribute you create: span names stay low-cardinality (template, never instance), attribute keys come from the registry in lowercase dot-namespaced form, and custom keys live in their own namespace. Apply when writing or reviewing tracing instrumentation; not for metrics, logs, backend choice, or sampling.
the name: no concrete ids, no full URLs, no query strings, no user names, no timestamps.
{method} {route template} — GET /users/{id},POST /checkout. The template keeps its placeholder; concrete values go into attributes. If no route template is available, the name is just the method (GET) — never the raw path.
{method} (plus the template only when the client truly knowsit). POST, not POST https://host/v2/pay?attempt=3.
{operation} {target} — SELECT invoices,INSERT shipment_events, find sessions. Never the query text as the name.
http.request.method — the method (GET) — not method, httpMethod, verbhttp.response.status_code — the code (200) — not status, statusCode, codehttp.route — the matched template (/users/{id})url.path — the concrete path (/users/4711) on the server sideurl.full — the complete URL on the client side (credentials redacted)server.address / server.port — the logical target host and portdb.system.name (postgresql, mysql, redis, mongodb),db.operation.name (SELECT, GET, find), db.collection.name (table or collection), and db.query.text only when capture is deliberate and parameterized.
error.type (exception classname, or the status code as a string) and set the span status to error. The human-readable message is never promoted into an attribute KEY.
in snake_case (status_code). Never camelCase, never spaces, never bare words.
app.cart.value, app.user.plan_tier, or acompany-domain namespace. Never invent keys inside a registry namespace (http.*, db.*, url.*, server.*), and never emit bare keys like userId or cartValue.
Server span name — instance data moves out of the name into attributes:
BEFORE tracer.start_span("GET https://api.shipfast.example/shipments/8842?expand=events")
AFTER tracer.start_span("GET /shipments/{shipment_id}")
span.set_attribute("http.route", "/shipments/{shipment_id}")
span.set_attribute("url.path", "/shipments/8842")
span.set_attribute("server.address", "api.shipfast.example")Attribute keys — invented camelCase becomes registry keys; the custom key gets a namespace:
BEFORE span.set_attribute("httpMethod", "GET")
span.set_attribute("statusCode", 200)
span.set_attribute("userId", "u-99")
AFTER span.set_attribute("http.request.method", "GET")
span.set_attribute("http.response.status_code", 200)
span.set_attribute("app.user.id", "u-99")Database span — the statement is not the name:
BEFORE with tracer.start_as_current_span("SELECT * FROM invoices WHERE customer_ref = %s"):
AFTER with tracer.start_as_current_span("SELECT invoices"):
span.set_attribute("db.system.name", "postgresql")
span.set_attribute("db.operation.name", "SELECT")
span.set_attribute("db.collection.name", "invoices")Failure recording — class, not prose, and no invented key:
BEFORE span.set_attribute("errorMessage", "timed out after 30s talking to fx.upstream.example")
AFTER span.set_attribute("error.type", "TimeoutError")
span.set_status(StatusCode.ERROR)method; the concrete path still goes to url.path.
url.full, with credentials stripped.
the name; put the action under a namespaced custom key (app.action), don't fork the name.
attribute, not a name suffix.
never emit both schemas from new code.
{method} {template}. Don't put ids, hosts, or query strings in a name.http.request.method / http.response.status_code. Don't coin httpMethod /statusCode / verb / code.
url.path. Don't overload the span name with it.db.system.name + db.operation.name. Don't make SQL text the span name.app.*). Don't emit bare userId / cartValue keys.getUser_4711).httpMethod, responseTimeMs, dbQueryString).errorMessage, failureReason) instead of error.type + status.http.user_id).{method} {template} (server), {method} (client), {operation} {target} (db).http.request.method, http.response.status_code, http.route, url.path,url.full, server.address.
db.system.name, db.operation.name, db.collection.name.error.type + error status; custom keys namespaced, lowercase, dot/snake.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +22 percentage points is the difference between those two pass rates over the 23 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.5-flash | verified | 7/10/2026 | +67% |
Other measured skills in the registry, with their headline benchmark lift.