Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Set up test environment variables
run: |
run: |
echo "POSTGRES_TEST_DSN=postgresql+asyncpg://a2a:a2a_password@localhost:5432/a2a_test" >> $GITHUB_ENV
echo "MYSQL_TEST_DSN=mysql+aiomysql://a2a:a2a_password@localhost:3306/a2a_test" >> $GITHUB_ENV

Expand All @@ -55,7 +55,7 @@ jobs:
run: |
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install dependencies
run: uv sync --dev --extra sql --extra encryption --extra grpc
run: uv sync --dev --extra sql --extra encryption --extra grpc --extra telemetry
- name: Run tests and check coverage
run: uv run pytest --cov=a2a --cov-report=xml --cov-fail-under=89
- name: Show coverage summary in log
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ To install with gRPC support:
uv add "a2a-sdk[grpc]"
```

To install with OpenTelemetry tracing support:

```bash
uv add "a2a-sdk[telemetry]"
```

To install with database support:

```bash
Expand Down Expand Up @@ -69,6 +75,12 @@ To install with gRPC support:
pip install "a2a-sdk[grpc]"
```

To install with OpenTelemetry tracing support:

```bash
pip install "a2a-sdk[telemetry]"
```

To install with database support:

```bash
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ dependencies = [
"fastapi>=0.115.2",
"httpx>=0.28.1",
"httpx-sse>=0.4.0",
"opentelemetry-api>=1.33.0",
"opentelemetry-sdk>=1.33.0",
"pydantic>=2.11.3",
"sse-starlette",
"starlette"
Expand All @@ -38,6 +36,7 @@ sqlite = ["sqlalchemy[asyncio,aiosqlite]>=2.0.0"]
sql = ["sqlalchemy[asyncio,postgresql-asyncpg,aiomysql,aiosqlite]>=2.0.0"]
encryption = ["cryptography>=43.0.0"]
grpc = ["grpcio>=1.60", "grpcio-tools>=1.60", "grpcio_reflection>=1.7.0", "protobuf==5.29.5", "google-api-core>=1.26.0"]
telemetry = ["opentelemetry-api>=1.33.0", "opentelemetry-sdk>=1.33.0"]

[project.urls]
homepage = "https://a2a-protocol.org/"
Expand Down
57 changes: 41 additions & 16 deletions src/a2a/utils/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,26 +59,58 @@ def internal_method(self):
import logging

from collections.abc import Callable
from typing import Any, TypeAlias
from typing import TYPE_CHECKING, Any

from opentelemetry import trace
from opentelemetry.trace import SpanKind as _SpanKind
from opentelemetry.trace import StatusCode

if TYPE_CHECKING:
from opentelemetry.trace import SpanKind as SpanKindType
else:
SpanKindType = object

SpanKind: TypeAlias = _SpanKind
logger = logging.getLogger(__name__)

try:
from opentelemetry import trace
from opentelemetry.trace import SpanKind as _SpanKind
from opentelemetry.trace import StatusCode

except ImportError:
logger.debug(
'OpenTelemetry not found. Tracing will be disabled. '
'Install with: \'pip install "a2a-sdk[telemetry]"\''
)

class _NoOp:
"""A no-op object that absorbs all tracing calls when OpenTelemetry is not installed."""

def __call__(self, *args: Any, **kwargs: Any) -> '_NoOp':
return self

def __enter__(self) -> '_NoOp':
return self

def __exit__(self, *args: object, **kwargs: Any) -> None:
pass

def __getattr__(self, name: str) -> '_NoOp':
return self

trace = _NoOp()
_SpanKind = _NoOp()
StatusCode = _NoOp()

SpanKind = _SpanKind
__all__ = ['SpanKind']

INSTRUMENTING_MODULE_NAME = 'a2a-python-sdk'
INSTRUMENTING_MODULE_VERSION = '1.0.0'

logger = logging.getLogger(__name__)


def trace_function( # noqa: PLR0915
func: Callable | None = None,
*,
span_name: str | None = None,
kind: SpanKind = SpanKind.INTERNAL,
kind: SpanKindType = SpanKind.INTERNAL,
attributes: dict[str, Any] | None = None,
attribute_extractor: Callable | None = None,
) -> Callable:
Expand Down Expand Up @@ -225,7 +257,7 @@ def sync_wrapper(*args, **kwargs) -> Any:
def trace_class(
include_list: list[str] | None = None,
exclude_list: list[str] | None = None,
kind: SpanKind = SpanKind.INTERNAL,
kind: SpanKindType = SpanKind.INTERNAL,
) -> Callable:
"""A class decorator to automatically trace specified methods of a class.

Expand Down Expand Up @@ -280,22 +312,15 @@ def not_traced_method(self):
exclude_list = exclude_list or []

def decorator(cls: Any) -> Any:
all_methods = {}
for name, method in inspect.getmembers(cls, inspect.isfunction):
# Skip Dunders
if name.startswith('__') and name.endswith('__'):
continue

# Skip if include list is defined but the method not included.
if include_list and name not in include_list:
continue
# Skip if include list is not defined but the method is in excludes.
if not include_list and name in exclude_list:
continue

all_methods[name] = method
span_name = f'{cls.__module__}.{cls.__name__}.{name}'
# Set the decorator on the method.
setattr(
cls,
name,
Expand Down
Loading
Loading