OpenTelemetry Semantic Conventions: Stable Attribute Names

OpenTelemetry semantic conventions are standardized attribute names and values for describing telemetry. They define a shared vocabulary so that traces, metrics, and logs from different languages, frameworks, and libraries can be queried the same way.

Conventions specify four things:

  • Attribute names — keys such as http.request.method, db.system.name, server.address
  • Attribute values — expected formats, such as GET for an HTTP method or postgresql for a database
  • Requirement levels — which attributes are Required, Conditionally Required, Recommended, or Opt-In
  • Namespaces — hierarchical naming such as service.name, service.version, service.instance.id

Without them, three services can describe the same HTTP request as http_method, http.verb, and request_method, and no dashboard or query works across all three.

Quick start

Most of the time you do not set convention attributes by hand. Instrumentation libraries emit them for you, and the only attributes you are responsible for are the ones describing your service.

Set service.name at minimum. Without it, your telemetry arrives as unknown_service.

go Go
import (
    "go.opentelemetry.io/otel/sdk/resource"
    semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
)

res, err := resource.New(ctx,
    resource.WithAttributes(
        semconv.ServiceName("checkout-api"),
        semconv.ServiceVersion("1.2.3"),
        semconv.DeploymentEnvironmentName("production"),
    ),
)
python Python
from opentelemetry.sdk.resources import Resource
from opentelemetry.semconv.attributes import service_attributes

resource = Resource.create({
    service_attributes.SERVICE_NAME: "checkout-api",
    service_attributes.SERVICE_VERSION: "1.2.3",
    "deployment.environment.name": "production",
})
javascript Node.js
const { resourceFromAttributes } = require('@opentelemetry/resources');
const {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} = require('@opentelemetry/semantic-conventions');

const resource = resourceFromAttributes({
  [ATTR_SERVICE_NAME]: 'checkout-api',
  [ATTR_SERVICE_VERSION]: '1.2.3',
  'deployment.environment.name': 'production',
});
java Java
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.ServiceAttributes;

Resource resource = Resource.getDefault()
    .merge(Resource.create(Attributes.of(
        ServiceAttributes.SERVICE_NAME, "checkout-api",
        ServiceAttributes.SERVICE_VERSION, "1.2.3"
    )));

The same values can be set without touching code, through OpenTelemetry environment variables:

shell
export OTEL_SERVICE_NAME=checkout-api
export OTEL_RESOURCE_ATTRIBUTES=service.version=1.2.3,deployment.environment.name=production

Attribute names changed during stabilization

Before writing any instrumentation, know that HTTP and database attributes were renamed when those conventions became stable. Most tutorials, Stack Overflow answers, and older code still use the pre-stable names.

The pattern behind the renames: flat names became namespaced, and protocol-specific names were replaced by shared ones. http.host became server.address because the same attribute now describes any network peer, not only HTTP.

HTTP

Old nameCurrent name
http.methodhttp.request.method
http.status_codehttp.response.status_code
http.urlurl.full
http.targeturl.path and url.query
http.schemeurl.scheme
http.hostserver.address
http.flavornetwork.protocol.version
http.user_agentuser_agent.original
http.client_ipclient.address
http.request_content_lengthhttp.request.body.size
http.response_content_lengthhttp.response.body.size

Databases

Old nameCurrent name
db.systemdb.system.name
db.namedb.namespace
db.operationdb.operation.name
db.statementdb.query.text
db.sql.tabledb.collection.name
db.connection_stringremoved
db.userremoved

Resources

Old nameCurrent name
deployment.environmentdeployment.environment.name

If you cannot migrate everything at once, see Uptrace normalization below — it maps old names onto new ones at query time.

Resource attributes

Resource attributes describe the entity producing telemetry. They are set once at startup and attached to every span, metric, and log from that process.

AttributeDescriptionExample
service.nameLogical service namecheckout-api
service.versionService version1.2.3
service.namespaceService namespaceshop
service.instance.idUnique instance identifiercheckout-api-7f8b9c-abc123
deployment.environment.nameDeployment environmentproduction, staging
host.nameHostnameweb-server-01
container.nameContainer namecheckout-api-container
k8s.pod.nameKubernetes pod namecheckout-api-7f8b9c-abc123
cloud.providerCloud provideraws, gcp, azure
cloud.regionCloud regionus-east-1

service.name is the only required one. Pick a name that stays stable across deployments — it is the primary key for grouping telemetry in every backend.

HTTP

HTTP conventions are stable. Instrumentation libraries set these automatically; the tables below are for reading telemetry and for writing your own instrumentation.

Server spans

AttributeRequirementExample
http.request.methodRequiredGET, POST
url.pathRequired/api/users/12345
url.schemeRequiredhttps
http.response.status_codeConditionally Required200, 404, 500
http.routeConditionally Required/api/users/:id
server.addressRecommendedapi.example.com
server.portConditionally Required443
client.addressRecommended192.168.1.100
user_agent.originalRecommendedMozilla/5.0...
network.protocol.versionRecommended1.1, 2

Use http.route for the span name and for grouping, not url.path. A route such as /api/users/:id has a bounded set of values, while url.path produces a new value for every user id. See cardinality below.

go Go
import (
    "go.opentelemetry.io/otel/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
)

ctx, span := tracer.Start(ctx, "GET /api/users/:id",
    trace.WithSpanKind(trace.SpanKindServer),
)
defer span.End()

span.SetAttributes(
    semconv.HTTPRequestMethodKey.String("GET"),
    semconv.HTTPRoute("/api/users/:id"),
    semconv.URLPath("/api/users/12345"),
    semconv.URLScheme("https"),
    semconv.ServerAddress("api.example.com"),
)

span.SetAttributes(semconv.HTTPResponseStatusCode(200))
python Python
from opentelemetry import trace
from opentelemetry.semconv.attributes import (
    http_attributes,
    url_attributes,
    server_attributes,
)

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span(
    "GET /api/users/:id",
    kind=trace.SpanKind.SERVER,
) as span:
    span.set_attributes({
        http_attributes.HTTP_REQUEST_METHOD: "GET",
        http_attributes.HTTP_ROUTE: "/api/users/:id",
        url_attributes.URL_PATH: "/api/users/12345",
        url_attributes.URL_SCHEME: "https",
        server_attributes.SERVER_ADDRESS: "api.example.com",
    })

    span.set_attribute(http_attributes.HTTP_RESPONSE_STATUS_CODE, 200)
javascript Node.js
const { trace, SpanKind } = require('@opentelemetry/api');
const {
  ATTR_HTTP_REQUEST_METHOD,
  ATTR_HTTP_ROUTE,
  ATTR_HTTP_RESPONSE_STATUS_CODE,
  ATTR_URL_PATH,
  ATTR_URL_SCHEME,
  ATTR_SERVER_ADDRESS,
} = require('@opentelemetry/semantic-conventions');

const span = tracer.startSpan('GET /api/users/:id', {
  kind: SpanKind.SERVER,
  attributes: {
    [ATTR_HTTP_REQUEST_METHOD]: 'GET',
    [ATTR_HTTP_ROUTE]: '/api/users/:id',
    [ATTR_URL_PATH]: '/api/users/12345',
    [ATTR_URL_SCHEME]: 'https',
    [ATTR_SERVER_ADDRESS]: 'api.example.com',
  },
});

span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, 200);
span.end();

Client spans

AttributeRequirementExample
http.request.methodRequiredGET, POST
url.fullRequiredhttps://api.example.com/users/123
server.addressRequiredapi.example.com
server.portRequired443
http.response.status_codeConditionally Required200, 404
network.protocol.versionRecommended1.1, 2

Client spans carry url.full rather than url.path, because the full target is what identifies an outgoing call. Strip credentials from the URL before recording it.

go Go
ctx, span := tracer.Start(ctx, "GET",
    trace.WithSpanKind(trace.SpanKindClient),
)
defer span.End()

span.SetAttributes(
    semconv.HTTPRequestMethodKey.String("GET"),
    semconv.URLFull("https://api.example.com/users/123"),
    semconv.ServerAddress("api.example.com"),
    semconv.ServerPort(443),
)

resp, err := http.Get("https://api.example.com/users/123")
if err != nil {
    span.RecordError(err)
    span.SetStatus(codes.Error, err.Error())
    return err
}
defer resp.Body.Close()

span.SetAttributes(semconv.HTTPResponseStatusCode(resp.StatusCode))
python Python
import requests
from opentelemetry import trace
from opentelemetry.semconv.attributes import (
    http_attributes,
    url_attributes,
    server_attributes,
)

with tracer.start_as_current_span("GET", kind=trace.SpanKind.CLIENT) as span:
    span.set_attributes({
        http_attributes.HTTP_REQUEST_METHOD: "GET",
        url_attributes.URL_FULL: "https://api.example.com/users/123",
        server_attributes.SERVER_ADDRESS: "api.example.com",
        server_attributes.SERVER_PORT: 443,
    })

    response = requests.get("https://api.example.com/users/123")

    span.set_attribute(
        http_attributes.HTTP_RESPONSE_STATUS_CODE, response.status_code
    )

Databases

Database conventions are stable. Note that db.connection_string and db.user were removed rather than renamed — connection strings routinely carry credentials.

AttributeRequirementExample
db.system.nameRequiredpostgresql, mysql, mongodb, redis
db.namespaceConditionally Requiredusers_db
db.operation.nameConditionally RequiredSELECT, INSERT, findOne
db.collection.nameConditionally Requiredusers
db.query.textRecommendedSELECT * FROM users WHERE id = ?
server.addressRecommendeddb.example.com
server.portRecommended5432
go Go (PostgreSQL)
ctx, span := tracer.Start(ctx, "SELECT users",
    trace.WithSpanKind(trace.SpanKindClient),
)
defer span.End()

span.SetAttributes(
    semconv.DBSystemNameKey.String("postgresql"),
    semconv.DBNamespace("users_db"),
    semconv.DBOperationName("SELECT"),
    semconv.DBCollectionName("users"),
    semconv.DBQueryText("SELECT * FROM users WHERE id = $1"),
)

rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", userID)
if err != nil {
    span.RecordError(err)
    span.SetStatus(codes.Error, err.Error())
}
python Python (MongoDB)
from opentelemetry import trace

with tracer.start_as_current_span("findOne users", kind=trace.SpanKind.CLIENT) as span:
    span.set_attributes({
        "db.system.name": "mongodb",
        "db.namespace": "users_db",
        "db.operation.name": "findOne",
        "db.collection.name": "users",
        "db.query.text": '{"id": "?"}',
    })

    result = collection.find_one({"id": user_id})
javascript Node.js (Redis)
const { trace, SpanKind } = require('@opentelemetry/api');

const span = tracer.startSpan('GET', {
  kind: SpanKind.CLIENT,
  attributes: {
    'db.system.name': 'redis',
    'db.operation.name': 'GET',
    'db.query.text': 'GET user:?',
  },
});

try {
  const value = await redis.get('user:12345');
} finally {
  span.end();
}

Sanitize db.query.text before recording it. Replace literals with placeholders, as in the examples above — query text otherwise leaks user data into traces and inflates span size.

Messaging

AttributeRequirementExample
messaging.systemRequiredkafka, rabbitmq, aws_sqs, gcp_pubsub
messaging.operation.nameRequiredsend, receive, process
messaging.operation.typeConditionally Requiredsend, receive, process, settle
messaging.destination.nameConditionally Requiredorders-topic
messaging.message.idRecommendedmsg-123456
messaging.message.body.sizeRecommended1024
go Go (Kafka producer)
ctx, span := tracer.Start(ctx, "send orders-topic",
    trace.WithSpanKind(trace.SpanKindProducer),
)
defer span.End()

span.SetAttributes(
    semconv.MessagingSystemKey.String("kafka"),
    semconv.MessagingOperationName("send"),
    semconv.MessagingDestinationName("orders-topic"),
)

err := producer.SendMessage(&sarama.ProducerMessage{
    Topic: "orders-topic",
    Value: sarama.StringEncoder(message),
})
python Python (RabbitMQ consumer)
with tracer.start_as_current_span(
    "process notifications-queue",
    kind=trace.SpanKind.CONSUMER,
) as span:
    span.set_attributes({
        "messaging.system": "rabbitmq",
        "messaging.operation.name": "process",
        "messaging.destination.name": "notifications-queue",
        "messaging.message.id": message.message_id,
    })

    process_notification(message)

Messaging spans only form a connected trace if the producer injects trace context into message headers and the consumer extracts it. See OpenTelemetry context propagation.

RPC and gRPC

RPC conventions are still in development, but the attribute names below have been stable in practice.

AttributeRequirementExample
rpc.systemRequiredgrpc, java_rmi, dotnet_wcf
rpc.serviceRecommendedmyservice.EchoService
rpc.methodRecommendedEcho
rpc.grpc.status_codeConditionally Required0 (OK), 2 (UNKNOWN)
go
ctx, span := tracer.Start(ctx, "myservice.EchoService/Echo",
    trace.WithSpanKind(trace.SpanKindClient),
)
defer span.End()

span.SetAttributes(
    semconv.RPCSystemKey.String("grpc"),
    semconv.RPCService("myservice.EchoService"),
    semconv.RPCMethod("Echo"),
)

resp, err := client.Echo(ctx, req)
if err != nil {
    if st, ok := status.FromError(err); ok {
        span.SetAttributes(semconv.RPCGRPCStatusCodeKey.Int(int(st.Code())))
    }
    span.RecordError(err)
}

Generative AI

Attributes for LLM and agent instrumentation use the gen_ai.* namespace: gen_ai.system, gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, and others.

Two things matter when you look them up:

  • They moved. As of semantic conventions v1.42.0, all gen_ai.* attributes, metrics, events, and spans were deprecated in the main repository and now live in a dedicated GenAI conventions repository. Links into semantic-conventions/docs/gen-ai are stale.
  • They are not stable. The GenAI conventions are in development and change faster than the core conventions, which is why they were split out. Pin what you depend on and expect renames.

For instrumenting LLM applications with these attributes, see OpenTelemetry for AI systems.

Naming rules

If no convention covers your case, follow the same rules the project uses.

  1. Namespace with dots: app.cache.hit, not app_cache_hit
  2. Use snake_case inside a namespace segment: http.response.status_code
  3. Prefer nesting over flat compound names: http.request.body.size, not http.request_body_size
  4. Use singular nouns: http.request.method, not http.request.methods
  5. Avoid abbreviations: user_agent.original, not ua.original
  6. Namespace your own attributes with your company or application name, so they never collide with future conventions

Rule 3 is the one that changed. Early conventions used flat names such as http.request_content_length; the stable conventions nest them as http.request.body.size.

For values: enumerations are lowercase strings, booleans are true/false rather than 1/0, and durations are floating-point seconds.

Cardinality

Cardinality is the number of distinct values an attribute can take. Every distinct combination of attribute values on a metric creates a separate time series, so unbounded attributes are the main cause of runaway storage and slow queries.

Bounded, safe as attributes

  • http.request.method — a handful of values
  • http.response.status_code — around 60 values
  • db.system.name — a fixed list
  • http.route — one per route in your application

Unbounded, keep off metrics

  • url.path — a new value per resource id
  • user.id, session.id — one per user or session
  • client.address — one per client

Unbounded values are acceptable on spans and logs, which are stored individually rather than aggregated. They are not acceptable as metric attributes. See cardinality management for how to drop or aggregate them.

Semantic convention packages

Each language ships generated constants so you do not type attribute names by hand. Using them means a rename shows up as a compile error rather than a silently broken dashboard.

LanguagePackage
Gogo.opentelemetry.io/otel/semconv/vX.Y.Z
Pythonopentelemetry-semantic-conventions
Node.js@opentelemetry/semantic-conventions
Javaio.opentelemetry:opentelemetry-semconv
.NETOpenTelemetry.SemanticConventions
Rubyopentelemetry-semantic_conventions
PHPopen-telemetry/sem-conv
shell Go
go get go.opentelemetry.io/otel/semconv/v1.34.0
shell Python
pip install opentelemetry-semantic-conventions
shell Node.js
npm install @opentelemetry/semantic-conventions
xml Java
<dependency>
    <groupId>io.opentelemetry.semconv</groupId>
    <artifactId>opentelemetry-semconv</artifactId>
    <version>1.34.0</version>
</dependency>

The Go package is versioned by import path: semconv/v1.34.0 and semconv/v1.30.0 can coexist in one build, which makes upgrades incremental. Other languages ship one version per package release.

Versioning and migration

Semantic conventions are versioned and released separately from the OpenTelemetry specification. Stable attributes do not change meaning within a major version; attributes still in development can be renamed in any release.

When upgrading:

  1. Read the changelog for renames before bumping the version
  2. Upgrade one signal at a time — resource attributes first, then spans, then metrics
  3. Update saved queries, dashboards, and alerts that reference renamed attributes
  4. Expect a transition period where both old and new names appear, because instrumentation libraries upgrade independently of your application code

Uptrace normalization

Uptrace applies normalization rules so that telemetry from sources on different convention versions can be queried together.

Attribute names are stored with dots replaced by underscores, for compatibility with tools such as Prometheus and Grafana Tempo: service.name becomes service_name.

You select a semantic convention version on the Project Settings page. Uptrace then renames incoming attributes to that version, so a service still emitting db_statement and a service emitting db_query_text both answer the same query. Existing rules are not modified, to avoid breaking queries that already work.

v1.33.0

Normalized attr nameCandidates
deployment_environment_namedeployment_environment, environment, env
service_nameservice, appname, application_name, fly_app_name
host_namehostname, host_hostname, host
db_namespacedb_name, db_cassandra_keyspace, db_hbase_namespace, dbname
db_system_namedb_system, db_type, db_dbms
db_collection_namedb_sql_table
db_operation_namedb_operation
db_query_textdb_statement
http_request_methodhttp_method, request_method, method
http_routehttp_server_route, route
log_severityseverity, error_severity, log_level, level
log_sourcesource_type, facility
log_file_pathlog_filepath, log_file
code_function_namecode_function
code_file_pathcode_filepath
code_line_numbercode_lineno
process_pidprocid, pid
server_addressnet_host_name, http_server_name, http_host
client_addresshttp_client_ip, net_peer_name, ip
messaging_message_idmessage_id, msgid
grouping_fingerprintlog_fingerprint, exception_fingerprint

v1.25.0

Normalized attr nameCandidates
deployment_environmentdeployment_environment_name, environment, env
service_nameservice, appname, application_name, fly_app_name
host_namehostname, host_hostname, host
enduser_iduser_id, user_identifier, user
db_namedb_cassandra_keyspace, db_hbase_namespace, dbname
db_systemdb_type, db_dbms
http_request_methodhttp_method, request_method, method
http_routehttp_server_route, route
http_response_status_codehttp_status_code
http_request_body_sizehttp_request_content_length
http_response_body_sizehttp_response_content_length
log_severityseverity, error_severity, log_level, level
log_sourcesource_type, facility
log_file_pathlog_filepath, log_file
cloud_regionfly_region
cloud_resource_idfaas_id
cloud_availability_zonecloud_zone
process_pidprocid, pid
url_schemehttp_scheme
url_fullhttp_url
url_pathhttp_target, request
faas_invocation_idfaas_execution
user_agent_originalhttp_user_agent, browser_user_agent, user_agent
network_protocol_namenet_app_protocol_name, messaging_protocol, http_flavor
network_protocol_versionnet_app_protocol_version, messaging_protocol_version
network_transportnet_transport
server_addressnet_host_name, http_server_name, http_host
server_portnet_host_port
server_socket_domainnet_sock_peer_name
server_socket_addressnet_host_ip, net_sock_host_addr
server_socket_portnet_host_port, net_sock_host_port
client_addresshttp_client_ip, net_peer_name, ip
client_socket_addressnet_peer_ip, net_sock_peer_addr
client_socket_portnet_peer_port, net_sock_peer_port
messaging_destination_namemessaging_destination
messaging_destination_kindmessaging_destination_kind
messaging_message_idmessage_id, msgid
messaging_message_typemessage_type
messaging_message_payload_size_bytesmessage_uncompressed_size
grouping_fingerprintlog_fingerprint, exception_fingerprint

Indexed attributes

Uptrace stores data in ClickHouse, a columnar database. To speed up queries, some attributes are stored in dedicated columns instead of a generic attribute map. Attributes outside this list still work in queries, but filtering on an indexed attribute is faster.

AttributeSpansLogsEventsPreaggregated
deployment_environmentYesYesYesYes
service_namespaceYesYesYesYes
service_nameYesYesYesYes
service_versionYesYesYesYes
host_nameYesYesYesYes
telemetry_sdk_nameYesYes
telemetry_sdk_languageYesYes
telemetry_sdk_versionYesYes
telemetry_auto_versionYesYes
otel_library_nameYesYesYes
otel_library_versionYesYesYes
client_addressYes
client_socket_addressYes
client_socket_portYes
db_systemYes
db_nameYes
db_sql_tableYes
db_statementYes
db_operationYes
process_pidYesYes
process_commandYesYes
process_runtime_nameYesYes
process_runtime_versionYesYes
process_runtime_descriptionYesYes
log_severityYes
log_file_pathYes
log_file_nameYes
log_iostreamYes
log_sourceYes
exception_typeYes
exception_messageYes
messaging_message_idYes
messaging_message_typeYes
messaging_message_payload_size_bytesYes

What's next?