Skip to content

Python: Allow programmatic OTel service name, resource attributes, and OTLP exporter config - #7703

Open
Dineshsuriya D (droideronline) wants to merge 4 commits into
microsoft:mainfrom
droideronline:python/otel-programmatic-config
Open

Python: Allow programmatic OTel service name, resource attributes, and OTLP exporter config#7703
Dineshsuriya D (droideronline) wants to merge 4 commits into
microsoft:mainfrom
droideronline:python/otel-programmatic-config

Conversation

@droideronline

Copy link
Copy Markdown
Contributor

Motivation & Context

#7702

Description & Review Guide

configure_otel_providers() — the documented one-call entry point for setting up OpenTelemetry — only read service.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-level create_resource() helper already supported passing service_name/service_version/**attributes programmatically. 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 .env file.

This PR:

  • Adds 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.
  • Threads these through to create_resource() (for the resource attributes) and through _get_exporters_from_env()_create_otlp_exporters() (for the OTLP exporter settings).
  • Keeps precedence consistent with the OTel spec: explicit parameters override the corresponding base environment variable, but signal-specific environment variables (e.g. OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) still take precedence over both.
  • Adds compression support ("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.
  • Forwards timeout/compression as-is (including None) to the OTLP exporter constructors — when left unset, the exporter classes themselves already fall back to reading OTEL_EXPORTER_OTLP_TIMEOUT/OTEL_EXPORTER_OTLP_COMPRESSION, so no env var re-parsing was needed there.
  • Leaves mTLS/certificate options out of scope for this convenience layer; those remain available by constructing exporters directly and passing them via the existing configure_otel_providers(exporters=...) escape hatch.

Example of what's now possible without touching any environment variables:

fromagent_framework.observabilityimportconfigure_otel_providersconfigure_otel_providers(
service_name="my-app",
service_version="1.0.0",
resource_attributes={"deployment_environment": "production"},
otlp_endpoint="https://otel-collector.example.com:4317",
otlp_headers={"Authorization": "Bearer <token>"},
otlp_compression="gzip",
)

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, and configure_otel_providers() forwarding both the resource and OTLP parameters end-to-end.

Related Issue

Fixes#7702

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

…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=...).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@agent-framework-automationagent-framework-automationBot added the python Usage: [Issues, PRs], Target: Python label Aug 17, 2026
Comment threadpython/packages/core/agent_framework/observability.py
Comment threadpython/packages/core/agent_framework/observability.py Outdated
Comment threadpython/packages/core/agent_framework/observability.py Outdated
- 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.
@moonbox3

Copy link
Copy Markdown
Contributor

/review

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment threadpython/packages/core/agent_framework/observability.py
Comment threadpython/packages/core/agent_framework/observability.py
Comment threadpython/packages/core/agent_framework/observability.py Outdated
…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.
@moonbox3

Copy link
Copy Markdown
Contributor

/review

@github-actions

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
observability.py11098292%431–433, 435–436, 441–442, 447–449, 451–452, 457–458, 541, 543–544, 547, 550, 553–554, 559, 563–564, 574–575, 585–586, 597, 599–600, 603, 606, 609–610, 615, 619–620, 631–632, 643–644, 656, 820, 941–942, 1205, 1221–1222, 1428–1429, 1713, 2014–2015, 2126, 2278, 2321–2322, 2511, 2752–2753, 2757, 2797–2798, 2802, 2843–2846, 2887–2888, 3039, 3042, 3054, 3071, 3075–3076, 3079, 3085, 3205, 3426, 3428
TOTAL47180437390%

Python Unit Test Overview

TestsSkippedFailuresErrorsTime
957336 💤0 ❌0 🔥2m 31s ⏱️

@moonbox3

Copy link
Copy Markdown
Contributor

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.
@droideronline

Copy link
Copy Markdown
ContributorAuthor

Fixed — both were caused by the last commit's new code, not flaky:

  • Pre-commit Hooks: pyupgrade wanted _shield_env's return annotation simplified from Generator[None, None, None] to Generator[None] (py310+ target). Fixed.
  • Package Checks (pyright): resource_attributes.update(attributes) after isinstance(attributes, Mapping) narrowing hit reportUnknownArgumentType, since the declared parameter type (Mapping[str, Any] | Any) collapses under Any and pyright narrows a bare Mapping isinstance check to Mapping[Unknown, Unknown]. Fixed with an explicit cast("Mapping[str, Any]", attributes).

Verified locally: pyupgrade --py310-plus is now a no-op, pyright reports 0 errors on observability.py (with the grpc/http OTLP exporter packages installed to match CI), ruff check/format clean, and all 230 tests in test_observability.py still pass. Pushed.

@moonbox3

Copy link
Copy Markdown
Contributor

Tao Chen (@TaoChenOSU) can you look at this one please?

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pythonUsage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: configure_otel_providers() has no way to set service name, resource attributes, or OTLP exporter config programmatically

3 participants

@droideronline@moonbox3