Python: Allow programmatic OTel service name, resource attributes, and OTLP exporter config - #7703
Conversation
…d OTLP exporter config in configure_otel_providers() Previously, configure_otel_providers() only read service.name, resource attributes, OTLP endpoint, protocol, headers, timeout, and compression from environment variables (OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES, OTEL_EXPORTER_OTLP_*), even though the lower-level create_resource() helper already supported passing these programmatically. This forced any caller of the documented one-call setup entry point to rely on env vars/.env files for basic telemetry identification and exporter configuration. Add service_name, service_version, resource_attributes, otlp_endpoint, otlp_protocol, otlp_headers, otlp_timeout, and otlp_compression keyword arguments to configure_otel_providers() and ObservabilitySettings, threaded through to create_resource() and the OTLP exporter construction path. Explicit parameters take precedence over the corresponding base environment variable; signal-specific environment variables (e.g. OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) still take precedence over both, matching standard OTel env var rules. mTLS/certificate options remain out of scope for this convenience layer; those can still be set by constructing exporters directly and passing them via configure_otel_providers(exporters=...).
- create_resource(): apply OTEL_RESOURCE_ATTRIBUTES as a base before explicit service_name/service_version/attributes, instead of overlaying it last, so an explicit value is never silently replaced by the environment. - create_resource(): accept resource attributes via a new attributes= dict parameter, in addition to **kwargs, so callers can pass a dictionary whose keys might collide with create_resource's own parameter names (e.g. service_name, env_file_path) without raising TypeError. Update _configure_providers() to pass resource_attributes via attributes= instead of unpacking it into **kwargs. - configure_otel_providers(): consolidate the service_name/service_version/ resource_attributes/otlp_* assignments that were duplicated across the env-file and non-env-file branches into a single shared block, since these fields don't need the env-file loading that the rest of the branch exists for. Addresses PR review comments from @moonbox3.
|
/review |
There was a problem hiding this comment.
MAF Automated Review — Iteration 1
Result: Findings reported
Scope: full PR (2 commit(s)): bcc1b96dbc0e, d2a162166467
Model: gpt-5.6-sol
Overview
The PR provides a coherent programmatic configuration path and adds useful precedence and forwarding tests, including signal-specific endpoint behavior and resource-attribute collision handling. The strongest guards are protocol normalization, protocol-specific compression mapping, optional-dependency errors, and the existing one-time setup guard. Residual risks remain around empty header overrides, credentials crossing to an environment-selected endpoint, and compatibility for callers that previously used attributes as an ordinary resource key.
Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
3 verified findings remained after source verification (1 high, 2 medium) across 1 file. Details are attached to the affected lines below.
Affected areas: python/packages/core/agent_framework/observability.py
…PR review
- _create_otlp_exporters()/_get_exporters_from_env(): an explicit
otlp_headers={} override was collapsed to None before reaching the OTLP
exporter constructors, which then fell back to reading
OTEL_EXPORTER_OTLP_HEADERS themselves (an empty dict is just as falsy as
None to their own "headers or environ.get(...)" fallback). This silently
resurrected an environment-configured credential the caller explicitly
tried to suppress. Fixed by distinguishing "not resolved" (None, exporter
may check env) from "resolved to nothing" (an authoritative {}, must not
check env) throughout, and added _shield_env()/_construct_otlp_exporter()
to temporarily hide the relevant header env vars for the duration of
construction when a signal's headers were authoritatively resolved.
- _get_exporters_from_env(): when both a programmatic otlp_endpoint and
otlp_headers are given, withhold those headers from any signal whose
endpoint resolves to a different origin (e.g. because a stray
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT points elsewhere), so a credential meant
for one collector can't be sent to a different, unintended host. Pure
env-var-driven configuration (OTEL_EXPORTER_OTLP_ENDPOINT +
OTEL_EXPORTER_OTLP_HEADERS) is unaffected and keeps its existing,
spec-conformant behavior.
- create_resource(): the new attributes= parameter previously assumed a
mapping and called dict.update() on whatever was passed, which raised
ValueError for the pre-existing call shape create_resource(attributes=
"some_value") (before attributes had a dedicated parameter, it was only
reachable via **kwargs and set a literal resource attribute named
"attributes"). Non-mapping values are now handled the same way as before,
preserving that call shape alongside the new mapping form.
Added 10 regression tests covering all three issues, verified end-to-end
against the real opentelemetry-exporter-otlp-proto-grpc/http packages.
Addresses automated review comments on PR microsoft#7703.
|
/review |
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
|
Dineshsuriya D (@droideronline) please fix the failing CI/CD issues |
- pyupgrade: simplify _shield_env's return annotation from Generator[None, None, None] to Generator[None], matching the codebase's existing style (py310-plus target). - pyright: cast the isinstance(attributes, Mapping)-narrowed value to Mapping[str, Any] before calling dict.update(), since attributes' declared type (Mapping[str, Any] | Any) collapses under Any and pyright otherwise narrows a bare Mapping isinstance check to Mapping[Unknown, Unknown], triggering reportUnknownArgumentType. Verified locally: pyupgrade --py310-plus is a no-op, pyright reports 0 errors on observability.py (with the grpc/http OTLP exporter packages installed, matching CI), ruff check/format clean, and all 230 tests in test_observability.py still pass.
|
Fixed — both were caused by the last commit's new code, not flaky:
Verified locally: |
|
Tao Chen (@TaoChenOSU) can you look at this one please? |
Motivation & Context
#7702
Description & Review Guide
configure_otel_providers()— the documented one-call entry point for setting up OpenTelemetry — only readservice.name, resource attributes, and OTLP exporter settings (endpoint/protocol/headers/timeout/compression) from environment variables (OTEL_SERVICE_NAME,OTEL_RESOURCE_ATTRIBUTES,OTEL_EXPORTER_OTLP_*), even though the lower-levelcreate_resource()helper already supported passingservice_name/service_version/**attributesprogrammatically. Any caller of the documented setup path had no way to configure these in code and was forced to rely on process environment variables or a.envfile.This PR:
service_name,service_version,resource_attributes,otlp_endpoint,otlp_protocol,otlp_headers,otlp_timeout, andotlp_compressionkeyword arguments toconfigure_otel_providers()andObservabilitySettings.create_resource()(for the resource attributes) and through_get_exporters_from_env()→_create_otlp_exporters()(for the OTLP exporter settings).OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) still take precedence over both.compressionsupport ("gzip"/"deflate"/"none"), mapped to the correct enum for gRPC (grpc.Compression) vs HTTP (opentelemetry.exporter.otlp.proto.http.Compression) exporters, since the two packages use different enum types.timeout/compressionas-is (includingNone) to the OTLP exporter constructors — when left unset, the exporter classes themselves already fall back to readingOTEL_EXPORTER_OTLP_TIMEOUT/OTEL_EXPORTER_OTLP_COMPRESSION, so no env var re-parsing was needed there.configure_otel_providers(exporters=...)escape hatch.Example of what's now possible without touching any environment variables:
Added tests covering:
create_resource()/_get_exporters_from_env()param overrides taking precedence over env vars, signal-specific env vars still winning over a programmatic base override, andconfigure_otel_providers()forwarding both the resource and OTLP parameters end-to-end.Related Issue
Fixes #7702
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.