OTel Tracing Attributes Cheat Sheet
Stop inventing your own keys. The semantic conventions that backends already understand, the ones that actually move the needle on debugging, and the cardinality rules that keep your bill sane.
HTTP
The HTTP conventions are the most stable and the most useful. Auto-instrumentation libraries set most of these for you, verify they actually fire before adding more.
http.request.method,GET,POST, etc. Uppercase, by spec.http.response.status_code, integer. Don't store as string.url.full, complete URL including query. Strip auth tokens before recording.url.path, path only. Use the routed path (/users/:id), not the literal (/users/4521), literal blows up cardinality.server.address+server.port, the destination host.client.addressfor the inbound caller's IP on a server span.http.route, the framework-level route template. The single most useful HTTP attribute for grouping.user_agent.original, the User-Agent header verbatim. Don't add a parsed version; backends do that.
RPC & gRPC
For gRPC, the conventions exist; for proprietary RPC, follow the same shape. Don't invent.
rpc.system,grpc,connect_rpc,apache_dubbo, etc. Required.rpc.service, the fully qualified service name (com.example.AccountService).rpc.method, method name only (GetUser). Service + method together identify the call.rpc.grpc.status_code, the gRPC numeric status (0=OK, 5=NOT_FOUND, etc.). Add as integer.- For streaming, span events for each message keep span counts manageable. Don't open one span per message.
- Set span
kind=clienton the caller side andkind=serveron the callee. Backends rely on this for service-graph rendering.
Database
Database spans are the highest-value tracing investment. Latency lives here; the attributes below are what make queries findable.
db.system.name,postgresql,mysql,redis,mongodb, etc.db.namespace, database name (Postgres) or schema. The unit of multi-tenancy.db.collection.name, table or collection. Required for any meaningful aggregation.db.operation.name,SELECT,INSERT,findAndModify. The verb.db.query.text, sanitized query (parameters replaced with?). Never put raw values; cardinality and PII both bite.db.response.status_code, for engines that return one. Postgres SQLSTATE goes here.- Span name should be
<operation> <collection>, e.g.,SELECT users. Not the full query; that'sdb.query.text's job.
Messaging
Producer and consumer spans share a trace via context propagation in headers. The conventions below make Kafka, RabbitMQ, SQS all look the same in your backend.
messaging.system,kafka,rabbitmq,aws_sqs,nats, etc.messaging.destination.name, topic, queue, or exchange name. The grouping key.messaging.operation.type,publish,receive,process,create,settle. Drives spankind.messaging.message.id, per-message identifier. Useful for end-to-end tracing across batches.messaging.kafka.message.offset,messaging.kafka.partition, Kafka-specific, but very common.- Always inject context into headers on publish; always extract on consume. Without it, producer and consumer spans live in different traces.
- For batch consumers, one span per batch with
messaging.batch.message_count; span events for individual failures inside.
Custom attribute rules
You'll need custom attributes. The rules below keep them readable and prevent the registry-of-doom.
- Namespace everything,
company.tenant.id, nottenant_id. Avoids collisions when two libraries pick the same name. - Lowercase, dot-separated,
snake_casewithin segments.billing.invoice_id, notbillingInvoiceId. - Keep the value type stable, if it's an integer, always integer; never sometimes string.
- Never put PII in attributes. Hash it, prefix with
company.user.id_hash, or omit. PII in traces is a compliance event waiting to happen. - Use span events for things that happen during the span (cache miss, retry attempt). Attributes are for stable facts about the span.
- Don't duplicate resource attributes onto every span.
service.nameanddeployment.environment.nameare resource-level; the SDK propagates them automatically.
Cardinality discipline
Your trace bill is mostly cardinality. Every unique combination of attribute values is a row in the backend's index. Discipline here is the difference between $200/month and $20k/month.
- Bounded. Status codes, HTTP methods, route templates, finite sets. Safe.
- Bounded-ish. Tenant IDs, region, environment, growing slowly with the business. Usually fine.
- Dangerous. User IDs, request IDs, full URLs, raw queries. High-cardinality, either omit or move to span events.
- Attribute count per span, aim for < 30. Above that you're paying for noise.
- Sampling at the SDK is your real cost lever.
parentbased_traceidratioat 10% is a good starting place; adjust per service. - Tail-based sampling at the collector keeps 100% of errors and slow traces while dropping the boring middle. Best of both worlds for any non-trivial deployment.