Unit economics needs a denominator the business counts. This chapter is about the numerator, and it is harder than it looks for a reason that has nothing to do with discipline.
Attribution is usually taught as a tagging problem: agree on a taxonomy, enforce it, done. That framing survives about one quarter. The failure is not that people forget to tag. It is that the billing system and the tracing system have different key spaces, different granularities, and different definitions of "required" — and the join between them is where the number goes wrong.
The three clouds do not agree on what a tag is
Every cost report you will ever build starts from a resource label. Here is what the three providers actually permit, quoted from their own documentation:
| AWS tags | GCP labels | Azure tags | |
|---|---|---|---|
| Max per resource | 50 user-created | 64 | 50 name-value pairs |
| Key length | 1–128 Unicode | 1–63 | up to 512 |
| Value length | 0–256 Unicode | 0–63 | up to 256 |
| Case | case sensitive | lowercase only | case-insensitive for operations |
| Reserved | aws: prefix, not counted |
— | — |
Read the case row again, because it is the expensive one.
Take the key CostCenter. On AWS, CostCenter, costcenter and Costcenter are three distinct tags producing three separate allocation lines. On Azure, they are one tag — tag names are case-insensitive for operations, though the resource provider may keep whichever casing you sent, and that is the casing you see in cost reports. On GCP the question does not arise, because label keys and values may contain only lowercase letters, numeric characters, underscores and dashes; CostCenter is not a legal key at all.
This is not a subtle trap that only bites the careless. AWS's own tagging guidance tells you to decide whether you will use Costcenter, costcenter or CostCenter and apply that choice consistently — the vendor documents the footgun because it goes off often enough to warrant a warning.
The portable key is the intersection, and it is GCP's rules
If you operate on more than one cloud — and the neocloud chapter is largely about why you might — the tag scheme that survives is the strictest common subset, not the most generous one:
- 50 keys per resource (AWS and Azure floor)
- 63 characters for keys and 63 for values (GCP floor on both)
- lowercase, digits, underscore, dash only (GCP charset)
Which is to say: design labels to GCP's rules and they work everywhere. Design them to Azure's and they are illegal on GCP and case-split on AWS. The Azure key-length allowance is 8.13× GCP's, and every character of that headroom is unusable if you ever want the same taxonomy to describe both estates.
That single sentence is most of the practical content of a multi-cloud tagging standard, and it costs nothing to adopt on day one and a migration to adopt on day four hundred.
Not everything is taggable, and the residue is not noise
The second structural limit: Azure states plainly that not all resource types support tags, and that classic resources such as Cloud Services do not support them at all. AWS reserves the aws: prefix for system tags you cannot edit or delete. Every estate therefore has a floor of unattributable spend that no amount of tagging policy will reach.
The correct response is not to chase it to zero. It is to measure the residue and report it as a number. An attribution report that quietly allocates the untaggable remainder across teams — pro rata by usage, say — is manufacturing precision it does not have. A report whose headline is "N% attributed, the remainder unattributable, and here is the inventory of what sits in the remainder" is doing the job. The unattributed share is the honest headline metric of an attribution programme, and it should be shrinking or explained.
Deliberately no illustrative percentage there. Any number this book printed would be invented, and invented numbers in a worked example are the ones readers remember.
The trace side is younger than you think
Cost tells you the total. Traces tell you which request caused it. Joining them is the whole game, and the standard for the AI half is not finished.
OpenTelemetry's generative-AI semantic conventions define gen_ai.client.token.usage, a histogram in units of {token} measuring "Number of input and output tokens used", split by a required gen_ai.token.type attribute taking input or output. That is exactly the primitive you want: token counts, typed, on the same span as everything else you already trace.
Three caveats matter more than the metric does.
It is marked Development, not Stable. The status badge on the token-usage metric is Development. Building a chargeback system on an unstable attribute name is a decision, not an oversight, and it should be written down as one.
The convention moved repositories. The GenAI conventions were relocated out of the main semantic-conventions repository into a dedicated semantic-conventions-genai repository, and the old paths now carry a redirect notice. Anything you pinned to the old location is stale.
Emission is conditional by design. The specification says the metric SHOULD be reported when an operation uses tokens and the count is readily available, and gen_ai.request.model is only conditionally required — "if available". Those are correct engineering choices for a spec that must cover streaming and non-streaming providers. They also mean a compliant instrumentation can legitimately produce spans with no token count and no model name, which is precisely the row your cost join will silently drop.
What to build
- One key, everywhere, in the portable subset. Lowercase, ≤63 characters,
[a-z0-9_-]. Put it on the resource and in the trace, with the same spelling. The join is only as good as the weakest of the two. - Join on the coarsest key that is reliably present, not the finest one you wish existed. Service beats request when request is 60% populated.
- Report coverage as a first-class metric. Percentage of spend attributed, percentage of traces carrying the key, and the population rate of
gen_ai.request.model. Falling coverage is the leading indicator; a wrong cost report is the lagging one. - Pin the convention version. Write down which semantic-convention version you instrumented against and which repository it came from, so the next person can tell drift from breakage.
- Never allocate the residue silently. If it must be spread, say it was spread, and show the pre-spread number next to it.
The honest limit of this chapter
Everything above is sourced from provider documentation and an open specification — that is, from what the systems permit. No figure here is a measurement of what attribution coverage real organisations achieve, because no vendor publishes that and this book does not invent benchmarks. If you have seen "typical coverage is 85%" quoted somewhere, ask where the sample came from; the answer is usually a survey of self-selected respondents, which is not the same kind of number as the limits in the table above.
The limits are checkable and stable enough to design against. The coverage figure you will actually hit is an empirical property of your own estate, and the first useful thing an attribution programme produces is that number, measured rather than assumed.