GET /accounts/{id} is one row, however many accounts
there are. The route comes from the http.route attribute, the route template your framework
matched, which OpenTelemetry’s HTTP instrumentations set on the request metric and the server span.
In the four examples
tests/otelapps sends a fixed load to each example and checks that every request is counted under
exactly these routes, with no raw path left over.
When your framework does not set it
Set it yourself in a middleware that runs after routing, on the active server span. This is what the Go example does, becauseotelhttp puts the route on metrics but not on the span:
http.route on the current span (and, where your instrumentation allows, on the metric
attributes). Use the template, never the actual path. The span attribute matters when you export
only traces, and for your own backend.
What the platform does with routes
Every route, whether you set it or not, goes through the same fixed normalization before it is stored:- No
http.route: the platform uses the request path (url.path, or the legacyhttp.target) instead, without the query string. - Values become placeholders, in the template and in a path alike: numbers become
{id}, UUIDs{uuid}, long hex strings and hashes{hex}or{hash}, dates{date}, tokens{token}, Stellar account, muxed account and contract keys{account},{muxed},{contract}, assets{asset}, email and federation addresses{email}and{federation}, and a segment longer than 64 bytes{long}. A secret seed in a path becomes{redacted-secret}, and the API traffic page warns you, because the seed was exposed in a URL. - At most 300 distinct routes and 50 services per project and UTC day (the service’s current
settings). Routes beyond that are counted under
{other}, so their requests are still in the totals, and the API traffic page shows a warning. A route that changes with every request (a slug, a name) fills the allowance quickly; sethttp.routeto the template to avoid that.
internal/routenorm lists them in order).