Configuring Resource Attributes for Python Services

Every span, metric point and log record a Python process emits carries a resource: the set of attributes that says which service, which version, which environment and which host produced it. Get it right once and every signal can be filtered and joined by the same identity; get it wrong — or let each signal configure it separately — and a service appears under three names in three tools. This page covers setting the resource once, from the environment, and sharing it across all three providers. It is a task article under OpenTelemetry SDK setup, part of the distributed tracing and OpenTelemetry in Python section.

One identity, three signals Two configurations of the same Python service are compared. In the first, each provider is configured separately: the tracer provider gets service.name checkout from an environment variable, the meter provider gets checkout-api from a constant somebody typed, and the logger provider gets no resource at all and reports unknown_service. A dashboard filtering by service name finds traces but no metrics, and logs appear under a service nobody recognises. In the second, one Resource object is built from the environment and passed to all three providers, so every span, metric point and log record carries exactly the same service name, version, environment and instance identifier, and filtering by any of them works across all three signals. The note records that the resource is sent once per export batch, so sharing it costs nothing. the same service, configured two ways three resources, configured separately traces: checkout metrics: checkout-api logs: unknown_service a filter on service.name finds one signal in three one Resource, shared Resource.create(from the environment) TracerProvider MeterProvider LoggerProvider every signal carries identical identity — and it is sent once per batch, so sharing costs nothing
A resource configured once and shared cannot disagree with itself. Three configured separately almost always eventually do.

Prerequisites

pip install "opentelemetry-sdk>=1.27.0,<2.0.0"
# set at deploy time, so one image reports correctly in every environment
export OTEL_SERVICE_NAME=checkout
export OTEL_RESOURCE_ATTRIBUTES="service.version=2026.09.18,deployment.environment=production"

Implementation

Step 1 — Take identity from the environment. OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES are read by Resource.create automatically. Setting them in the deployment manifest — the version from the build, the environment from the target — means the same container image reports as staging in staging and production in production, with no code change and no chance of a build embedding the wrong value.

Step 2 — Build one resource and pass it to every provider. Resource.create merges the environment variables, any attributes passed in code, and the SDK's own defaults into one object. Passing that one object to the tracer, meter and logger providers makes all three signals carry identical identity.

# telemetry/resource.py
import os
import socket

from opentelemetry.sdk.resources import Resource

RESOURCE = Resource.create({
    # constants that do not vary by deployment may live in code
    "service.namespace": "commerce",
    # a unique id per process: replicas and workers distinguishable
    "service.instance.id": f"{socket.gethostname()}-{os.getpid()}",
})
# telemetry/setup.py
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk._logs import LoggerProvider

from .resource import RESOURCE

tracer_provider = TracerProvider(resource=RESOURCE)
meter_provider = MeterProvider(resource=RESOURCE, metric_readers=[READER])
logger_provider = LoggerProvider(resource=RESOURCE)

Step 3 — Add host and platform attributes once. Facts about where the process runs — host name, container identifier, cloud region, cluster, namespace — can be detected by the SDK or added by the collector. Letting the collector's resource detection add them keeps applications simple and applies consistently to every service on a node, as in collector topology and deployment. Doing both usually produces two subtly different values for the same attribute.

Step 4 — Make service.instance.id unique per process. Under a prefork server, several processes share a host and a service name. An instance identifier per process distinguishes them on spans and logs. It must be computed after the fork, so each worker gets its own — the post-fork hook described in Gunicorn and Uvicorn worker logging is the place. On metrics it becomes part of every series' identity, which is fine for a handful of workers and a cardinality problem if instances churn rapidly.

Step 5 — Verify every signal agrees. Export one span, one metric point and one log record from the same process and compare their resources. They should be byte-for-byte identical on the identity attributes.

Expected Output: the resource block that appears once in each export batch.

{
  "resource": {
    "attributes": [
      {"key": "service.name", "value": {"stringValue": "checkout"}},
      {"key": "service.namespace", "value": {"stringValue": "commerce"}},
      {"key": "service.version", "value": {"stringValue": "2026.09.18"}},
      {"key": "deployment.environment", "value": {"stringValue": "production"}},
      {"key": "service.instance.id", "value": {"stringValue": "checkout-7d9f8c5b6-xk2lm-41"}},
      {"key": "telemetry.sdk.language", "value": {"stringValue": "python"}},
      {"key": "telemetry.sdk.version", "value": {"stringValue": "1.27.0"}}
    ]
  }
}
Where each attribute comes from Four sources of resource attributes are stacked in order of precedence. At the bottom are the SDK's built-in defaults: the telemetry SDK name, language and version, and a placeholder service name beginning with unknown_service. Above them, attributes passed in code to Resource.create. Above those, OTEL_RESOURCE_ATTRIBUTES from the environment. At the top, OTEL_SERVICE_NAME, which overrides any other source of service.name. A final layer outside the process shows the collector's resource detection adding host, container and cloud attributes after the telemetry leaves. The note records that because the environment sits above code, the deployment can correct a wrong value without rebuilding the image, and that the placeholder service name at the bottom is what every unconfigured service ends up reporting. later sources override earlier ones OTEL_SERVICE_NAME — wins for service.name OTEL_RESOURCE_ATTRIBUTES — version, environment, anything deploy-specific attributes passed in code — constants such as service.namespace SDK defaults — telemetry.sdk.*, service.name=unknown_service:… outside the process: collector detection adds host, container, cloud the environment beats code, so a deployment can correct a wrong value without a rebuild
Environment variables sit above code, which is what lets one image report correctly everywhere and lets a wrong value be fixed without a rebuild.

Which attributes matter, and why

The semantic conventions define many resource attributes. A handful carry most of the value, and knowing why each matters helps decide what to set.

service.name is the primary identity and the default grouping for every backend. Without it every service is unknown_service, which makes the whole fleet one indistinguishable blob. It should be stable across versions and environments — the same name in staging and production, filtered apart by environment rather than encoded into the name.

service.version is what makes before-and-after comparison possible. A latency regression attributed to a version is a regression with a cause; one that is not attributable requires correlating deploy times by hand. Setting it from the build's own version identifier, rather than a manually maintained string, keeps it accurate.

deployment.environment separates production from everything else. Without it, staging traffic appears in production dashboards and production error rates are diluted or inflated by test traffic.

service.instance.id distinguishes replicas and workers. It is what lets an investigation say "only this pod is slow" rather than "the service is slow", which is often the difference between a node problem and a code problem.

service.namespace groups related services — all of a team's services, or all of a product's. Useful in large fleets for filtering, and harmless in small ones.

Host, container and cloud attributes complete the picture of where telemetry came from and are best added by the collector, which knows them without the application having to.

Resource attributes on metrics

Resource attributes behave differently on metrics than on traces and logs, and the difference affects which attributes are safe to set.

On a span or a log record, the resource is descriptive: it is sent once per batch and attached to each record for filtering, and adding an attribute costs almost nothing. On metrics, backends that follow the Prometheus model turn resource attributes into labels — either directly, or through a target-info series joined to every metric. Each distinct combination of resource attribute values then contributes its own set of series. A resource that changes per process restart multiplies the metric series count by the number of restarts within the retention period.

service.instance.id is the attribute most affected. It is valuable on spans and logs, where it identifies the pod or worker involved in a request. On metrics, a value that changes whenever a pod is replaced creates a new set of series for every deploy, and the old ones linger until retention expires. For stable fleets with long-lived pods this is manageable; for fleets that recycle workers frequently, or scale up and down through the day, it can dominate the metrics store.

The usual arrangement is to keep instance identity on the resource for all signals and to drop or aggregate it for metrics at the collector, where dropping and aggregating metrics in the collector applies. Per-instance metric breakdowns remain available for a short window if needed, and the long-term store holds series keyed by service rather than by process.

When the resource is wrong in production

Resource problems tend to be discovered in a specific way: a dashboard is empty for one service, or a service appears under two names. Three checks locate the cause quickly.

First, read the environment the process actually has — OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES inside the running container, not in the manifest. A variable misspelled in the manifest, or overridden by a base image, is the most common cause, and it is visible only in the running process.

Second, confirm there is one resource. A second provider created somewhere — by an instrumentation library initialised before the application's own setup, or by a test fixture left in production code — carries its own resource, often the default. Searching the codebase for provider constructors finds it.

Third, check the collector. A resource processor that overwrites service.name, or a detector that adds a conflicting host.name, changes identity after the telemetry leaves the process. Comparing a record's resource at the exporter with the same record in the backend shows whether anything changed it in between.

Which attribute answers which question A table mapping incident questions to the resource attribute that answers them. Which service is failing is answered by service.name. Did the last deploy cause it is answered by service.version, compared across the deploy time. Is it production or staging is answered by deployment.environment. Is it one pod or all of them is answered by service.instance.id or k8s.pod.name. Is it one zone or region is answered by cloud.availability_zone and cloud.region. The note says each missing attribute is a question nobody can answer from telemetry during an incident. question during an incident attribute that answers it which service is failing? service.name did the last deploy cause it? service.version production or staging? deployment.environment one pod or all of them? service.instance.id · k8s.pod.name one zone or everywhere? cloud.availability_zone · cloud.region each missing attribute is a question nobody can answer during an incident set them once in the resource and every span, metric and log record carries them
Resource attributes are the dimensions an incident is sliced by. Each one maps onto a question someone will ask.

Configuration options

Attribute Source Note
service.name OTEL_SERVICE_NAME stable across versions and environments
service.version build version, via env enables release comparison
deployment.environment deploy target, via env separates production traffic
service.instance.id host + pid, after fork per-process identity
service.namespace code constant groups related services
Host / container / cloud collector detection added once, consistently
One Resource object shared by all providers signals cannot disagree

Verification

Print the resource each provider actually holds.

from opentelemetry import trace, metrics
print(trace.get_tracer_provider().resource.attributes["service.name"])
print(metrics.get_meter_provider()._sdk_config.resource.attributes["service.name"])

Expected Output: the same name from both providers.

checkout
checkout

Common mistakes

No service name. Error signature: every service shown as unknown_service in the backend. Root cause: OTEL_SERVICE_NAME unset and no name in code. Remediation: set it in the deployment.

A resource per provider. Error signature: traces and metrics for the same service under different names. Root cause: providers configured separately. Remediation: build one Resource and pass it to all three.

Version baked into the image by hand. Error signature: dashboards attributing a regression to the wrong release. Root cause: a manually maintained version string. Remediation: take it from the build and pass it via the environment.

Environment in the service name. Error signature: checkout-staging and checkout-prod as separate services. Root cause: encoding the environment into the name. Remediation: one name, filtered by deployment.environment.

Instance id computed before fork. Error signature: every worker reporting the same instance. Root cause: the id computed in the master. Remediation: compute it after the fork.

Frequently Asked Questions

What happens if service.name is not set?

The SDK uses a placeholder beginning with unknown_service, so every service without a name appears as the same unknown service in the backend. Setting it is the single most important configuration step.

Should resource attributes be set in code or by environment variable?

By environment variable for anything that differs between deployments — version, environment, instance — so the same image reports correctly everywhere. Code is fine for constants such as the service name, and OTEL_SERVICE_NAME covers that too.

What is the difference between a resource attribute and a span attribute?

A resource attribute describes the process producing telemetry and is identical on every span, metric point and log record it emits. A span attribute describes one operation. Identity belongs on the resource; request details belong on spans.

Should the collector or the SDK add host and cloud attributes?

Either works. The collector's resource detection keeps applications simple and applies consistently to every service on the node. SDK detectors work without a collector. Using both risks two slightly different values for the same attribute, so pick one.