Why architecture matters here
The architectural payoff of a shared vocabulary is that it decouples the people producing telemetry from the people and tools consuming it. A dashboard author, an alert rule, or a backend vendor can rely on http.response.status_code meaning the same thing regardless of which team, language, or framework emitted it. Without conventions, every consumer must learn every producer's private naming, and every new service is a new integration. With them, conformance is the integration: emit the standard names and the whole ecosystem of queries, dashboards, and alerts works out of the box.
The resource-versus-signal distinction matters because the two answer different questions and are indexed differently. Resource attributes are effectively constant for a process, so backends use them as the grouping key — 'show me everything from the checkout service, version 2.3, in this cluster.' Signal attributes vary per event and are the slicing dimensions within that group — 'of the checkout service's spans, which route is slow.' Conflating the two (putting service.version on every span as a signal attribute, or trying to filter by route at the resource level) breaks both the grouping and the cost model, because resource attributes are stored once per stream while signal attributes are stored per event.
Stability levels matter because they tell you what you can build on. The registry marks each attribute as stable, experimental, or deprecated. A stable attribute is a promise that its name and meaning will not change under you, so it is safe to hard-code into an alert; an experimental one may be renamed, so building critical alerting on it is a risk you take knowingly. Treating all attributes as equally durable is how you wake up to a broken dashboard after an instrumentation upgrade renamed an experimental field.
Cardinality matters because attribute values, not just names, drive cost. The conventions are careful to specify low-cardinality where possible — http.route (the templated /users/{id}) rather than the raw high-cardinality URL, so that metrics aggregate sensibly. Putting a user ID or a full URL into a metric label explodes the number of time series and can bankrupt a metrics backend. The conventions encode hard-won guidance about which fields are safe as aggregation keys and which belong only on high-cardinality traces or logs.
Finally, versioning matters because an observability estate outlives any single version of its vocabulary. Names improve, typos get fixed, experimental fields stabilize under new names. Without a schema URL and schema transformations, every such change would either be impossible (freezing the vocabulary forever) or catastrophic (orphaning historical data and forcing a big-bang upgrade). The schema mechanism is what lets the conventions keep getting better while a decade of stored telemetry stays queryable — a property that only looks optional until the first rename you have to live through. The same discipline applies to your own extensions: a private attribute you add today may need to be renamed or promoted tomorrow, and treating custom attributes with the same versioning rigor as the public ones is what keeps a home-grown vocabulary from decaying into the very naming chaos the conventions exist to prevent.
The architecture: every piece explained
The convention registry is the specification itself: a structured catalog defining each attribute's fully qualified name, its type (string, int, boolean, array), its allowed or example values, whether it is required, recommended, or opt-in on a given signal, and its stability level. Attributes are grouped into domains — HTTP, database, messaging, RPC, GenAI, cloud, host, Kubernetes, and many more — and reused across signals so that the same db.system.name means the same thing on a trace span, a metric, and a log. The registry is machine-readable, which is what lets code generators emit constants for every language rather than developers hand-typing strings.
Resource attributes are assembled by resource detectors at startup and attached to every signal a process emits. service.name is the load-bearing one — it is required and is the primary identity of a service in every backend — accompanied by service.version, service.instance.id, and environment attributes discovered from the platform: host.*, cloud.*, k8s.*, container.*. Because they are identical across a process's signals, they are carried once per resource rather than repeated per event.
Signal attributes live on the individual span, metric data point, or log record and describe that specific operation. A server HTTP span carries http.request.method, http.route, http.response.status_code, url.path, and server.address; a database client span carries db.system.name, db.namespace, db.operation.name, and a sanitized db.query.text. The conventions also standardize span names and metric names, so that, for example, HTTP server duration is recorded under a defined metric name with defined bucket semantics.
The schema URL and versioning layer governs evolution. Emitted telemetry can carry a schema_url pointing at the exact version of the conventions it conforms to. When an attribute is renamed between versions, the project publishes a schema file describing the transformation, so a collector or backend can translate old names to new ones and unify data emitted by services on different versions. This decouples the upgrade schedule of individual services from the correctness of cross-service queries.
Custom attributes handle everything the conventions do not cover. The rule is namespacing: your organization puts its own attributes under a private prefix — com.yourcompany.* — that will never collide with a current or future OpenTelemetry name. This lets you extend the vocabulary for domain-specific needs without risking a clash when the conventions later add an attribute in the same conceptual space. The collector sits in the middle of all of this, and its processors can enforce conventions, drop or rename attributes, and apply schema transformations before export. The diagram below shows how the registry, the resource/signal split, versioning, and custom attributes relate.
End-to-end flow
Trace an HTTP request as telemetry. A request hits the checkout service. The auto-instrumentation for its web framework starts a server span and, following the HTTP conventions, tags it with http.request.method=POST, http.route=/cart/{id}/checkout, url.path=/cart/98/checkout, and server.address — using the templated route, not the raw path, as the low-cardinality attribute so metrics aggregate cleanly.
That span inherits the process's resource: service.name=checkout, service.version=2.3.1, and the Kubernetes attributes a resource detector discovered — k8s.pod.name, k8s.namespace.name, cloud.region. These are attached once to the resource, not repeated on every span, and they are what a backend will group by.
The request handler calls the orders database. The database-client instrumentation opens a child span and applies the database conventions: db.system.name=postgresql, db.namespace=orders, db.operation.name=INSERT, and a parameter-stripped db.query.text. Because these names are the same as those every other service emits for its database calls, a single 'slow database operations' query spans the whole fleet.
The spans, plus metrics like the standardized HTTP server duration histogram and any log records (which carry the same resource and correlate via trace context), flow to the collector. A processor there stamps or verifies the schema_url, and — if this service still emits an older attribute name that was since renamed — applies the published schema transformation to bring it in line with the version the backend expects, so data from a not-yet-upgraded service still lands under the current names.
In the backend, the payoff arrives. An operator opens a generic HTTP dashboard and it works immediately, because the checkout service used the conventional attribute names the dashboard queries. They filter by service.name to isolate checkout, slice by http.route to find the slow endpoint, group by db.system.name to see which datastore is the bottleneck, and correlate to the exact database span via trace context — all without knowing anything team-specific about how checkout was instrumented. The end-to-end property is that conformance to one vocabulary made the telemetry portable: the same queries, dashboards, and alerts work across every service, language, and backend that speaks the conventions.