Skip to content

fix(tracing): avoid import-time tracing exporter/provider initialization#2490

Closed
OiPunk wants to merge 3 commits intoopenai:mainfrom
OiPunk:codex/openai-agents-2489-lazy-tracing-init
Closed

fix(tracing): avoid import-time tracing exporter/provider initialization#2490
OiPunk wants to merge 3 commits intoopenai:mainfrom
OiPunk:codex/openai-agents-2489-lazy-tracing-init

Conversation

@OiPunk
Copy link
Contributor

@OiPunk OiPunk commented Feb 15, 2026

Summary

Fixes #2489 by removing import-time tracing initialization side effects.

This change makes default tracing objects lazy:

  • BackendSpanExporter is now created only when first needed.
  • BatchTraceProcessor is now created only when first needed.
  • Global TraceProvider is initialized on first get_trace_provider() access instead of at module import.

This avoids constructing httpx.Client and thread primitives during import, which is safer for fork-based runtimes and pre-fork server startup paths.

Changes

  • src/agents/tracing/processors.py
    • Replace eager globals with lazy singletons for exporter/processor.
  • src/agents/tracing/__init__.py
    • Add lazy default provider initialization wrapper.
    • Remove import-time set_trace_provider(...), add_trace_processor(...), and atexit.register(...) side effects.
  • tests/tracing/test_lazy_tracing_init.py
    • Add regression tests for import-time laziness and idempotent initialization behavior.

Validation

  • uv run --with ruff ruff check src/agents/tracing/__init__.py src/agents/tracing/processors.py tests/tracing/test_lazy_tracing_init.py
  • env -u ALL_PROXY -u all_proxy -u HTTPS_PROXY -u https_proxy -u HTTP_PROXY -u http_proxy -u NO_PROXY -u no_proxy uv run --with pytest pytest -q tests/tracing/test_lazy_tracing_init.py tests/test_trace_processor.py (22 passed)
  • Changed executable lines in touched source files are covered (100%).

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af1a0f9b58

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@OiPunk
Copy link
Contributor Author

OiPunk commented Feb 15, 2026

Thanks for the CI signal. I pushed bc47daca to fix the mypy complaint in the new test helper by typing the json.loads return value via cast.

Local re-check on this follow-up commit:

  • uv run --with ruff ruff check tests/tracing/test_lazy_tracing_init.py
  • uv run mypy tests/tracing/test_lazy_tracing_init.py
  • env -u ALL_PROXY -u all_proxy -u HTTPS_PROXY -u https_proxy -u HTTP_PROXY -u http_proxy -u NO_PROXY -u no_proxy uv run --with pytest pytest -q tests/tracing/test_lazy_tracing_init.py tests/test_trace_processor.py (22 passed)

@OiPunk
Copy link
Contributor Author

OiPunk commented Feb 15, 2026

Thanks for the review, this was a valid catch.

I pushed commit 43e8b51 to make setup.get_trace_provider() lazily initialize the default provider and processor on first use. This keeps import-time behavior side-effect free, but restores expected runtime behavior for helper entry points like agents.trace(...), which now no longer fail when tracing has not been manually initialized yet.

I also added regression coverage in tests/tracing/test_lazy_tracing_init.py for:

  • trace(...) lazy initialization path
  • set_trace_provider(...) global assignment path

Local checks run on this update:

  • uv run --with ruff ruff check src/agents/tracing/setup.py tests/tracing/test_lazy_tracing_init.py
  • uv run mypy src/agents/tracing/setup.py tests/tracing/test_lazy_tracing_init.py
  • uv run --with pytest pytest -q tests/tracing/test_lazy_tracing_init.py tests/test_trace_processor.py
  • uv run --with coverage --with pytest coverage run -m pytest -q tests/tracing/test_lazy_tracing_init.py tests/test_trace_processor.py
  • uv run --with coverage coverage report -m src/agents/tracing/setup.py (100%)

@seratch seratch added this to the 0.9.x milestone Feb 16, 2026
@seratch
Copy link
Member

seratch commented Feb 16, 2026

@codex review again

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 43e8b512d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 21 to +30
if GLOBAL_TRACE_PROVIDER is None:
raise RuntimeError("Trace provider not set")
# Lazily initialize defaults on first tracing API usage to avoid
# import-time side effects while keeping historical call behavior.
from .processors import default_processor
from .provider import DefaultTraceProvider

provider = DefaultTraceProvider()
GLOBAL_TRACE_PROVIDER = provider
provider.register_processor(default_processor())
atexit.register(provider.shutdown)

Choose a reason for hiding this comment

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

P1 Badge Serialize lazy TraceProvider initialization

get_trace_provider() uses an unsynchronized check-then-set on GLOBAL_TRACE_PROVIDER. Two threads hitting first-use concurrently can each create a DefaultTraceProvider, register processors, and register separate atexit shutdown callbacks; one instance is then overwritten. This can return different providers to callers and cause duplicate shutdown/export behavior under concurrent startup.

Useful? React with 👍 / 👎.

Comment on lines +365 to +374
if _global_exporter is None:
_global_exporter = BackendSpanExporter()
return _global_exporter


def default_processor() -> BatchTraceProcessor:
"""The default processor, which exports traces and spans to the backend in batches."""
global _global_processor
if _global_processor is None:
_global_processor = BatchTraceProcessor(default_exporter())

Choose a reason for hiding this comment

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

P2 Badge Lock lazy singleton creation for tracing exporter

default_exporter() and default_processor() lazily initialize globals without synchronization. Concurrent first calls can allocate multiple BackendSpanExporter / BatchTraceProcessor instances. Extra exporters create their own httpx.Client objects and may not be explicitly closed, leading to avoidable resource usage and inconsistent singleton expectations.

Useful? React with 👍 / 👎.

@seratch seratch marked this pull request as draft February 16, 2026 23:51
@seratch
Copy link
Member

seratch commented Feb 17, 2026

Closing this in favor of #2499

@seratch seratch closed this Feb 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracing module creates httpx.Client and threading primitives at import time (fork safety hazard)

2 participants