---
name: otel-span-naming
source: https://app.decimal.ai/s/otel-span-naming@1/SKILL.md
source_sha256: bb4a8b41fd42
---

# OpenTelemetry span and attribute naming

## Contract

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.

## Rules

1. **A span name identifies a class of operations, never one call.** Nothing high-cardinality in
   the name: no concrete ids, no full URLs, no query strings, no user names, no timestamps.

2. **HTTP server span name = `{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.

3. **HTTP client span name = `{method}`** (plus the template only when the client truly knows
   it). `POST`, not `POST https://host/v2/pay?attempt=3`.

4. **Database span name = `{operation} {target}`** — `SELECT invoices`,
   `INSERT shipment_events`, `find sessions`. Never the query text as the name.

5. **HTTP attribute keys come from the registry:**
   - `http.request.method` — the method (`GET`) — not `method`, `httpMethod`, `verb`
   - `http.response.status_code` — the code (`200`) — not `status`, `statusCode`, `code`
   - `http.route` — the matched template (`/users/{id}`)
   - `url.path` — the concrete path (`/users/4711`) on the server side
   - `url.full` — the complete URL on the client side (credentials redacted)
   - `server.address` / `server.port` — the logical target host and port

6. **Database attribute keys:** `db.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.

7. **Errors:** record the low-cardinality failure class under `error.type` (exception class
   name, 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.

8. **Naming grammar for every key:** lowercase; namespaces joined by dots; multi-word segments
   in snake_case (`status_code`). Never camelCase, never spaces, never bare words.

9. **Custom attributes get your own namespace** — `app.cart.value`, `app.user.plan_tier`, or a
   company-domain namespace. Never invent keys inside a registry namespace (`http.*`, `db.*`,
   `url.*`, `server.*`), and never emit bare keys like `userId` or `cartValue`.

## Worked examples

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)
```

## Edge cases & exceptions

- **No route template exists** (raw middleware, catch-all proxy): the span name is the bare
  method; the concrete path still goes to `url.path`.
- **Query strings:** never in the span name. On the client side the whole URL belongs in
  `url.full`, with credentials stripped.
- **One endpoint multiplexing many actions** (a single RPC-over-POST route): keep the route in
  the name; put the action under a namespaced custom key (`app.action`), don't fork the name.
- **Retries:** each attempt is its own span with the SAME name; the attempt number is an
  attribute, not a name suffix.
- **Migration from legacy keys:** rename in code and translate old keys at the collector —
  never emit both schemas from new code.

## Do / Don't

- Do name server spans `{method} {template}`. Don't put ids, hosts, or query strings in a name.
- Do use `http.request.method` / `http.response.status_code`. Don't coin `httpMethod` /
  `statusCode` / `verb` / `code`.
- Do put the concrete path in `url.path`. Don't overload the span name with it.
- Do record `db.system.name` + `db.operation.name`. Don't make SQL text the span name.
- Do namespace custom keys (`app.*`). Don't emit bare `userId` / `cartValue` keys.
- Do snake_case multi-word segments. Don't camelCase anything.

## Common mistakes

- Full URL as the span name — a new name per request, so nothing aggregates.
- Handler-function span names with instance data baked in (`getUser_4711`).
- camelCase keys (`httpMethod`, `responseTimeMs`, `dbQueryString`).
- Free-text message keys (`errorMessage`, `failureReason`) instead of `error.type` + status.
- Custom data shoved into registry namespaces (`http.user_id`).
- The whole SQL statement as the database span name.

## Quick checklist

- Span name: `{method} {template}` (server), `{method}` (client), `{operation} {target}` (db).
- No ids, URLs, query strings, or messages in any span name.
- HTTP keys: `http.request.method`, `http.response.status_code`, `http.route`, `url.path`,
  `url.full`, `server.address`.
- DB keys: `db.system.name`, `db.operation.name`, `db.collection.name`.
- Failures: `error.type` + error status; custom keys namespaced, lowercase, dot/snake.
