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.
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"}}
]
}
}
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.
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.