Why do OBI HTTP spans carry high-cardinality names, and how does the OpenTelemetry semantic convention bound span name cardinality?
The span name is the single highest-cardinality field a trace system carries, so the OpenTelemetry HTTP semantic convention pins the name to {method} {target} and requires a low-cardinality target: http.route on the server side, url.template on the client side. The convention is explicit that an instrumentation must not default to using the raw URI path as the target. OpenTelemetry eBPF Instrumentation (OBI) builds its HTTP span names with exactly this shape - a method plus a route - and the only input that makes the name high-cardinality is whatever fills that route slot. The high-cardinality symptom is not the default; it is what the unmatched: path option produces when it is selected, and it is precisely the one setting the convention forbids as a default.
A span name that grows with the number of unique URLs you serve is a cardinality problem, not a correctness one: it is the route value that decides whether two requests collapse into one name or fan out into one name each.
The mechanism
-
The convention's name rule. The HTTP semantic convention says an HTTP span name SHOULD be
{method} {target}when a low-cardinalitytargetis available, and SHOULD be just{method}when it is not. The{method}ishttp.request.method, or the literalHTTPwhen the method is outside the known-methods set. ThetargetSHOULD behttp.routefor a server span orurl.templatefor a client span, and it notes thaturl.templateis the low-cardinality template of a path (for example/users/{id}rather than/users/42). The hard guardrail is the last sentence of that section: instrumentation must not default to using the URI path as the target. -
OBI names an HTTP span as
{method} {route}. Inpkg/appolly/app/request/span.go, theTraceNamebuilder for HTTP event types starts from the method - emitting the method as-is, orHTTPwhen the method is not in the known-methods list - and appends the route only when a route has been resolved. A span with no resolved route is therefore just{method}. The resolved route is the same value OBI exports ashttp.routeon a server span andurl.templateon a client span, so the span name and the exported attribute stay consistent. -
Where the route comes from. In
pkg/transform/routes.gothe route is matched off the URL path, in order: the user-definedroutes.patternsmatcher, then any per-service custom matchers, then a harvested route matcher. When none of those matched, theunmatchedpolicy decides the target. That is the knob that sets cardinality:unsetleaves the target empty so the name is just{method};wildcardcollapses the target to a single token;heuristicclusters similar paths into a low-cardinality template;low-cardinalitydoes the same clustering and additionally bounds it with a path-segment cap; andpathsets the route to the raw URL path. -
Why
pathis the high-cardinality case. Whenunmatchedispath, the code assigns the raw path directly to the route, so every distinct URL path becomes a distinct span name: one name per/users/42, one per/orders/1934, and so on. That is an unbounded set that grows with your traffic, which is the cardinality blow-up. The shipped default is theheuristicclustering instead, which keeps the name set bounded; thelow-cardinalityoption tightens that further with a per-service segment cap. The convention's "must not default to the URI path" is the reasonpathis an opt-in, never the default.
Verification and debugging path
-
Read the
routes.unmatchedvalue first. The default isheuristic, so a fresh OBI deployment already clusters paths into bounded names. If span names are one-per-URL, look for an explicitroutesblock that setsunmatchedtopath(or an empty value that fell through to the wildcard in an older layout) - that is the field to change. -
Watch the span name and the route attribute together. For a busy path, list the distinct
http.route/url.templatevalues, not the fullurl.full. Bounded naming shows a handful of template values; raw-path naming shows one value per distinct URL. The span name and that attribute should agree, because they are built from the same route value. -
Cap the metric series independently. Even with bounded names, the span-name metric series is capped by the metric span-name aggregation limit, so a run of pathological names degrades to a truncated metric set rather than an unbounded one. The limit bounds the damage but does not fix the naming.
The limitation
- The convention's cardinality rule is a mix of SHOULD and MUST: the "must not default to the URI path" is the one hard constraint; choosing route over template, and picking the low-cardinality option, are SHOULD-level guidance, so an operator can still opt into raw-path names with
unmatched: path. heuristicandlow-cardinalityare OBI-internal clustering strategies, not a spec: their output depends on the URL-clusterer and the per-service segment cap, so the exact template set for a given workload is OBI behavior, not something the semantic convention pins down.unsetremoves the target entirely (name is just{method}), the lowest-cardinality option, but it also drops the per-route distinction from the name, so you trade cardinality for detail.- This answer describes OBI's
main-branch behavior for HTTP spans. The route-matching pipeline and theunmatchedoption names are OBI specifics; the span-name cardinality rule itself is the shared semantic convention that any HTTP instrumentation follows.
References
- OpenTelemetry semantic conventions, HTTP span name rule - the
Namesection: the name is{method} {target}, the target SHOULD behttp.routeorurl.template, and instrumentation must not default to using the URI path as the target. - OBI
pkg/appolly/app/request/span.go-TraceNamebuilds the HTTP span name as the method plus the resolved route, with the method reduced toHTTPwhen it is unknown. - OBI
pkg/transform/routes.go- where the route is matched from the URL path and where theunmatchedpolicy, including the raw-path case, assigns the target. - OBI
pkg/obi/config.go- the shipped defaults:unmatchedset to the heuristic clustering, a*wildcard character, and a path-segment cardinality cap. - OBI span attribute catalog,
spans.md- the per-span-type attribute reference showing OBI emits HTTP client and server spans and carries the URL and route attributes.
Community discussion today
The two opt-in CNCF OpenTelemetry instrumentation channels supplied a three-message frozen window this run. The substantive thread, and the one this entry answers, is a practitioner flagging that, as OBI's HTTP span naming is currently proposed, the span name can end up high-cardinality - in short, that a span whose target is the raw per-request URL path is a distinct name for every unique path, which is exactly the cardinality blow-up the HTTP semantic convention's "must not default to the URI path" guardrail exists to prevent. The working follow-up in that thread is to bound the target to a route or template rather than the raw path.
The same window carried one separate, unresolved line - a request for a pointer to a "skill refinement" item that was mentioned - which is its own thread and is not folded into this answer.
Channel coverage for this run: the two opt-in archives supplied three messages. The visible-browser-only sources - the eunomia-bpf and sched-ext Discord servers, the bpf mailing list, and r/eBPF - could not be reviewed this run because no visible-browser session was available, so they are marked not-covered, not quiet.
Continue exploring
Back to index
eBPF Q&A
Daily, source-grounded answers to recurring questions about eBPF, Linux observability, profiling, runtime extension, and security.
Previous
When does a BPF LRU hash map evict an entry, and what changes when its LRU list is per-CPU instead of shared across CPUs?
Eviction happens only on the insert or update path, and only once the table has already reached maxentries.
Next
Why can an OpenTelemetry OBI metric stay in the store up to twice as long as its TTL, and does the Prometheus exporter drop the same label set on every scrape?
The answer splits on the exporter. OBI caps per-label-set metric cardinality in both its OpenTelemetry and Prometheus exporters with one shared TTL, but the two exporters delete at two different moments. The OTEL
- Last updated
- Oct 20, 2026
- First published
- Oct 20, 2026
- Contributors
- Yunwei 123
Was this page helpful?