Semantic conventions
Resource attributes, HTTP/DB/messaging conventions, and why consistent naming is what makes traces, metrics, logs, and profiles correlate at all.
Every chapter up to this point has produced spans, metrics, log lines, or profiling samples and trusted that they would line up correctly in Grafana — a trace for an order showing up next to the right logs, a metric labeled with the right service name, a profile attributable to the right process. None of that correlation happens automatically because the signals are “related” in some abstract sense. It happens because every signal in this tutorial, regardless of which language emitted it, uses the same attribute names and the same attribute values for the same concepts. That shared vocabulary is what OpenTelemetry calls semantic conventions: a specification for what they are, where they come from, and why skipping them quietly breaks everything every later chapter depends on.
OpenTelemetry’s own documentation on the topic is the canonical reference and worth reading directly: opentelemetry.io/docs/concepts/semantic-conventions. What follows is the subset that matters for the services in this tutorial, and why the convention — not the specific names chosen — is what deserves attention.
The problem conventions solve
Imagine two engineers independently instrument order-svc and inventory-svc without a shared standard. One names the HTTP method attribute http.method; the other, having read a different blog post, names it httpMethod. One records the database system as postgres; the other records it as postgresql. One tags a span’s service identity as service; the other as service_name. Every one of these choices is individually reasonable. Collectively, they make cross-service analysis nearly impossible: a Grafana query or a Tempo search built against one service’s attribute names silently returns nothing for the other, not because the data is missing but because it is there under a different key. Dashboards fork per service. Alerting rules fork per service. The entire value of distributed tracing — seeing one request’s behavior across service boundaries — depends on treating “an HTTP call” or “a database query” as the same kind of thing no matter which service produced it, and that only works if the two services describe it with identical vocabulary.
Semantic conventions are OpenTelemetry’s answer: a versioned, publicly maintained specification for what an attribute describing a given concept should be named, what values it should take, and which attributes are required versus merely recommended for a given kind of operation. They cover resource-level identity (what process produced this data), HTTP calls, database calls, RPC calls, messaging operations, and a growing list of other domains. Every language’s OpenTelemetry SDK and its auto-instrumentation libraries implement the same conventions, which is precisely why the auto-instrumented HTTP spans from a Spring Boot service and a Quarkus service in this tutorial carry the same attribute names even though no one wrote matching instrumentation code by hand in both.
Resource attributes: who produced this data
The most foundational set of conventions governs Resource — the entity that produced a given piece of telemetry, attached once per process and then carried on every span, metric, and log record that process emits. Three resource attributes matter most for this tutorial:
service.name— the logical name of the service, e.g.order-quarkusorinventory-spring. This is the single most important attribute in the whole system: it is what lets Grafana group traces, metrics, and logs by service, and it is set explicitly per service instack/compose.yaml’sOTEL_SERVICE_NAMEenvironment variable rather than inherited from any shared default, specifically because it has to be unique to be useful.service.version— the deployed version of that service, set todevfor every service in this tutorial’s local stack via the sharedSERVICE_VERSIONenvironment variable. In a real deployment pipeline this would track the actual release version, letting you filter or compare telemetry across a rollout.deployment.environment— which environment produced the data:localfor everything in this tutorial, via theDEPLOY_ENVvariable in the shared Compose anchor. In a real system this would distinguishstagingfromproduction, which matters enormously once the same dashboards and alerting rules need to serve more than one environment without manual filtering.
These three live under the x-service-env anchor covered in the shared infrastructure chapter, merged into every domain service’s environment identically except for service.name, which is set per service. That is a direct, practical illustration of the convention at work: because every service populates the same three resource attributes with the same keys, a single Grafana query can filter “everything from deployment.environment=local” or “everything from order-quarkus specifically,” and it works identically whether the underlying service is written in Java or Python, because the resource attributes are not language-specific — they are part of the specification, not part of any one SDK’s defaults.
HTTP, database, RPC, and messaging conventions
Beyond resource attributes, OpenTelemetry defines conventions for the attributes that describe a specific operation — what a span or metric is actually about. The services in this tutorial exercise four of these domains directly, corresponding to the four transport protocols introduced in the e-commerce domain chapter:
HTTP conventions cover order-svc’s REST surface. A server span for POST /orders carries http.request.method, http.route (the matched route template, not the raw path with its parameters interpolated, which would blow up cardinality — see the cardinality and cost chapter for why that distinction matters), and http.response.status_code, alongside server.address and server.port identifying what the request was sent to. Because every language’s HTTP server instrumentation in this tutorial follows the same HTTP semantic conventions, a Grafana panel built to show “p99 latency by route” works unmodified regardless of which language track produced the span.
Database conventions cover every service’s Postgres queries. A database span carries db.system (postgresql), db.namespace identifying the database and schema, and db.query.text for the statement itself, using the same attribute vocabulary whether the query came from order’s JDBC driver, inventory’s async database client, or review’s Python driver. This is what makes it possible to ask “how much total time did this trace spend in Postgres” as a single query across spans emitted by three completely different client libraries in three different languages — the spans look the same in the one place it matters, their database-related attributes.
RPC conventions cover the two gRPC hops from order to inventory and from order to payment. A client span for a gRPC call carries rpc.system (grpc), rpc.service and rpc.method identifying shop.inventory.v1.InventoryService/Reserve or shop.payment.v1.PaymentService/Authorize by their full proto-qualified names, and rpc.grpc.status_code for the transport-level outcome. That last attribute is what lets a trace viewer distinguish a gRPC call that failed at the transport level from one that completed and returned an application-level decline (inventory’s Reserve returning reserved: false, or payment’s Authorize returning a decline_reason, are not RPC failures — they are successful calls carrying a negative business result, and the convention keeps those two very different situations from looking identical in a trace).
Messaging conventions cover the order.placed Kafka event and are the most consequential of the four for this tutorial, because they are what the context propagation chapter depends on. A producer span for publishing to Kafka and a consumer span for processing a message each carry messaging.system (kafka), messaging.destination.name (order.placed), and messaging.operation (publish or process), using attribute names that are consistent regardless of which service is producing or consuming. Consistency here is not a nicety — it is the entire mechanism by which Grafana can show a Kafka producer span in order-svc and a Kafka consumer span in shipping-svc as part of the same conceptual messaging operation on order.placed, which is a prerequisite for the trace continuing across that boundary at all once context propagation is correctly wired up.
Stability, versioning, and why names sometimes change
Semantic conventions are not a frozen vocabulary handed down once and never revisited. The specification assigns each attribute a stability level — experimental or stable — and the project has, on occasion, renamed an attribute outright once real-world usage exposed a problem with the original name. The HTTP conventions are the clearest example: earlier drafts used http.method and http.status_code; the stabilized conventions use http.request.method and http.response.status_code instead, specifically to make room for attributes that distinguish a request’s properties from a response’s properties as the convention grew to cover more of the request lifecycle. A service instrumented against an older experimental convention and a service instrumented against the current stable one would, in principle, hit exactly the naming mismatch this chapter opened with — which is why staying current with the specification’s stable releases matters as an ongoing practice, not a one-time setup step.
OpenTelemetry tracks this with a schema_url attached to a batch of telemetry, identifying which version of the conventions it was produced against, so that collectors and backends can interpret older and newer data correctly rather than guessing. This tutorial’s services all target the same current stable conventions, which sidesteps the need to reconcile schema versions across services — a convenience a longer-lived production system usually has to manage by hand as it upgrades its instrumentation libraries at different times across different teams.
Custom attributes alongside the standard ones
None of this means every attribute a span carries has to come from the specification. The manual spans chapter and the auto-vs-manual hybrid chapter both add business-specific attributes on top of what auto-instrumentation already provides — order-svc’s handler might attach the SKU and quantity it is processing, or inventory’s reservation logic might attach the remaining on-hand count after a reserve. None of that is covered by the HTTP, database, RPC, or messaging conventions, because none of it is a general cross-service concept; it is specific to this application’s domain.
The convention that still applies here is namespacing, not attribute naming. OpenTelemetry reserves bare, unprefixed attribute names and the common namespaces (http.*, db.*, rpc.*, messaging.*, and so on) for the specification itself, precisely so that a future version of the conventions can add a new standard attribute without risk of colliding with something an application team already invented for its own purposes. A custom attribute for this domain’s order SKU is safest named under its own namespace — order.sku rather than a bare sku, for instance — both to avoid a future collision and to make it immediately obvious to anyone reading a trace which attributes are part of the portable OpenTelemetry vocabulary and which are specific to this one application. Getting this distinction right is what lets a dashboard built against the standard HTTP and database attributes work unmodified across every service in this tutorial, while the domain-specific attributes layered on top still tell the fuller story of what each service was actually doing.
Why consistency is what makes correlation work
It is worth being explicit about the mechanism, because it is easy to treat “use the standard attribute names” as a stylistic preference rather than a functional requirement. Correlation across signals in Grafana — jumping from a trace span to the log lines emitted during that span, or from a trace to the metrics recorded for the same operation, or from a trace to the CPU profile captured while that span was executing — works by matching shared identifiers and shared resource attributes across signal types. A log line correlates to a trace because it carries that trace’s trace_id and span_id, using the same field names Tempo and Loki both expect. A metric correlates to a particular service’s traces because both carry the same service.name value. A profile correlates to a service’s other telemetry because Pyroscope tags it with the same resource attributes everything else uses.
None of these correlations are computed by inspecting the content of a log message or guessing at similarity between a metric name and a span name. They are exact-match joins on specific, conventionally named fields. If one service emitted service.name and another emitted serviceName, Grafana’s correlation links — the ones that let a reader click from a trace directly into its logs, wired up automatically in this tutorial’s Grafana datasource provisioning — would simply fail to find a match for that one service, with no error message, just an empty result where a reader expected to see related data. This is the quiet failure mode semantic conventions prevent: not a crash, not an exception, just telemetry that technically exists but cannot be found from any of the places a reader would naturally look for it.
This is also why adopting OpenTelemetry’s semantic conventions is worth more than inventing an equally sensible scheme from scratch, even one that is internally consistent across all six services in this tutorial. The specification’s conventions are the ones every off-the-shelf auto-instrumentation library already implements, the ones Grafana’s panels, correlation features, and the exemplar links between metrics and traces are built to expect, and the ones most backends — Tempo, Loki, Mimir, and every commercial observability vendor’s OTLP ingestion — are designed around. Following the published convention means this tutorial’s six services, three languages, and four protocols all produce telemetry that a tool built against the specification already knows how to render correctly, with zero custom mapping. Diverging from it, even consistently, means rebuilding that integration work by hand, and redoing it again for every new dashboard and every new tool added later.
Correlation builds directly on this foundation: now that every signal in this tutorial shares a common vocabulary of resource and operation attributes, Chapter 16 covers the concrete mechanics of moving between traces, logs, metrics, and profiles inside Grafana for a single request.