Skip to content

Commit d94a0be

Browse files
committed
Add support to disable telemetry instrumentation. Add supporting tests and xdist configuration to ensure module import tests can be run in the same group
1 parent cb7cdb3 commit d94a0be

4 files changed

Lines changed: 152 additions & 8 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ exclude = ["tests/"]
7272
testpaths = ["tests"]
7373
python_files = "test_*.py"
7474
python_functions = "test_*"
75-
addopts = "-ra --strict-markers"
75+
addopts = "-ra --strict-markers --dist loadgroup"
7676
markers = [
7777
"asyncio: mark a test as a coroutine that should be run by pytest-asyncio",
78+
"xdist_group: mark a test to run in a specific sequential group for isolation",
7879
]
7980

8081
[tool.pytest-asyncio]
@@ -93,6 +94,7 @@ dev = [
9394
"pytest-asyncio>=0.26.0",
9495
"pytest-cov>=6.1.1",
9596
"pytest-mock>=3.14.0",
97+
"pytest-xdist>=3.6.1",
9698
"respx>=0.20.2",
9799
"ruff>=0.12.8",
98100
"uv-dynamic-versioning>=0.8.2",

src/a2a/utils/telemetry.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@
1818
- Automatic recording of exceptions and setting of span status.
1919
- Selective method tracing in classes using include/exclude lists.
2020
21+
Configuration:
22+
- Environment Variable Control: OpenTelemetry instrumentation can be
23+
disabled using the `OTEL_A2A_SDK_INSTRUMENTATION_ENABLED` environment
24+
variable.
25+
26+
- Default: `true` (tracing enabled when OpenTelemetry is installed)
27+
- To disable: Set `OTEL_A2A_SDK_INSTRUMENTATION_ENABLED=false`
28+
- Case insensitive: 'true', 'True', 'TRUE' all enable tracing
29+
- Any other value disables tracing and logs a debug message
30+
2131
Usage:
2232
For a single function:
2333
```python
@@ -57,6 +67,7 @@ def internal_method(self):
5767
import functools
5868
import inspect
5969
import logging
70+
import os
6071

6172
from collections.abc import Callable
6273
from typing import TYPE_CHECKING, Any
@@ -74,11 +85,33 @@ def internal_method(self):
7485
from opentelemetry.trace import SpanKind as _SpanKind
7586
from opentelemetry.trace import StatusCode
7687

88+
otel_installed = True
89+
7790
except ImportError:
7891
logger.debug(
7992
'OpenTelemetry not found. Tracing will be disabled. '
8093
'Install with: \'pip install "a2a-sdk[telemetry]"\''
8194
)
95+
otel_installed = False
96+
97+
ENABLED_ENV_VAR = 'OTEL_A2A_SDK_INSTRUMENTATION_ENABLED'
98+
INSTRUMENTING_MODULE_NAME = 'a2a-python-sdk'
99+
INSTRUMENTING_MODULE_VERSION = '1.0.0'
100+
101+
# Check if tracing is enabled via environment variable
102+
env_value = os.getenv(ENABLED_ENV_VAR, 'true')
103+
otel_enabled = env_value.lower() == 'true'
104+
105+
# Log when tracing is explicitly disabled via environment variable
106+
if otel_installed and not otel_enabled:
107+
logger.debug(
108+
'A2A OTEL instrumentation disabled via environment variable '
109+
'%s=%r. Tracing will be disabled.',
110+
ENABLED_ENV_VAR,
111+
env_value,
112+
)
113+
114+
if not otel_installed or not otel_enabled:
82115

83116
class _NoOp:
84117
"""A no-op object that absorbs all tracing calls when OpenTelemetry is not installed."""
@@ -99,12 +132,9 @@ def __getattr__(self, name: str) -> Any:
99132
_SpanKind = _NoOp() # type: ignore
100133
StatusCode = _NoOp() # type: ignore
101134

102-
SpanKind = _SpanKind
135+
SpanKind = _SpanKind # type: ignore
103136
__all__ = ['SpanKind']
104137

105-
INSTRUMENTING_MODULE_NAME = 'a2a-python-sdk'
106-
INSTRUMENTING_MODULE_VERSION = '1.0.0'
107-
108138

109139
def trace_function( # noqa: PLR0915
110140
func: Callable | None = None,

tests/utils/test_telemetry.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import asyncio
2+
import importlib
3+
import sys
24

3-
from collections.abc import Generator
5+
from collections.abc import Callable, Generator
46
from typing import Any, NoReturn
57
from unittest import mock
68

@@ -30,6 +32,35 @@ def patch_trace_get_tracer(
3032
yield
3133

3234

35+
@pytest.fixture
36+
def reload_telemetry_module(
37+
monkeypatch: pytest.MonkeyPatch,
38+
) -> Generator[Callable[[str | None], Any], None, None]:
39+
"""Fixture to handle telemetry module reloading with env var control."""
40+
41+
def _reload(env_value: str | None = None) -> Any:
42+
if env_value is None:
43+
monkeypatch.delenv(
44+
'OTEL_A2A_SDK_INSTRUMENTATION_ENABLED', raising=False
45+
)
46+
else:
47+
monkeypatch.setenv(
48+
'OTEL_A2A_SDK_INSTRUMENTATION_ENABLED', env_value
49+
)
50+
51+
# Remove from sys.modules to force fresh top-level execution
52+
sys.modules.pop('a2a.utils.telemetry', None)
53+
54+
# importlib.import_module is often cleaner than 'import' in dynamic contexts
55+
module = importlib.import_module('a2a.utils.telemetry')
56+
return module
57+
58+
yield _reload
59+
60+
# Cleanup to ensure other tests aren't affected by a "poisoned" sys.modules
61+
sys.modules.pop('a2a.utils.telemetry', None)
62+
63+
3364
def test_trace_function_sync_success(mock_span: mock.MagicMock) -> None:
3465
@trace_function
3566
def foo(x, y):
@@ -198,3 +229,43 @@ def foo(self) -> str:
198229
assert obj.foo() == 'foo'
199230
assert hasattr(obj.foo, '__wrapped__')
200231
assert hasattr(obj, 'x')
232+
233+
234+
@pytest.mark.xdist_group(name='telemetry_isolation')
235+
@pytest.mark.parametrize(
236+
'env_value,expected_tracing',
237+
[
238+
(None, True), # Default: env var not set, tracing enabled
239+
('true', True), # Explicitly enabled
240+
('True', True), # Case insensitive
241+
('false', False), # Disabled
242+
('', False), # Empty string = false
243+
],
244+
)
245+
def test_env_var_controls_instrumentation(
246+
reload_telemetry_module: Callable[[str | None], Any],
247+
env_value: str | None,
248+
expected_tracing: bool,
249+
) -> None:
250+
"""Test OTEL_A2A_SDK_INSTRUMENTATION_ENABLED controls span creation."""
251+
telemetry_module = reload_telemetry_module(env_value)
252+
253+
is_noop = type(telemetry_module.trace).__name__ == '_NoOp'
254+
255+
assert is_noop != expected_tracing
256+
257+
258+
@pytest.mark.xdist_group(name='telemetry_isolation')
259+
def test_env_var_disabled_logs_message(
260+
reload_telemetry_module: Callable[[str | None], Any],
261+
caplog: pytest.LogCaptureFixture,
262+
) -> None:
263+
"""Test that disabling via env var logs appropriate debug message."""
264+
with caplog.at_level('DEBUG', logger='a2a.utils.telemetry'):
265+
reload_telemetry_module('false')
266+
267+
assert (
268+
'A2A OTEL instrumentation disabled via environment variable'
269+
in caplog.text
270+
)
271+
assert 'OTEL_A2A_SDK_INSTRUMENTATION_ENABLED' in caplog.text

uv.lock

Lines changed: 43 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)