Skip to content

feat(a2a): add loongsuite-instrumentation-a2a (#28) - #274

Open
RichardoMrMu wants to merge 14 commits into
alibaba:mainfrom
RichardoMrMu:feat/a2a-instrumentation
Open

RichardoMrMu wants to merge 14 commits into
alibaba:mainfrom
RichardoMrMu:feat/a2a-instrumentation

Conversation

@RichardoMrMu

Copy link
Copy Markdown
Contributor

What

Adds a new instrumentation package loongsuite-instrumentation-a2a, providing automatic OpenTelemetry instrumentation for the official A2A (Agent2Agent) Python SDK (a2a-sdk).

Closes #28 (roadmap #36 — "Add instrumentation for official A2A", contribution welcome).

How

Produces an ARMS gen-ai AGENT span (gen_ai.span.kind=AGENT, gen_ai.operation.name=invoke_agent) around each server-side agent turn, bracketing the user's AgentExecutor.execute invocation so that all downstream work nests under a single invoke_agent span with a shared trace_id. Records a2a.context_id, a2a.task_id, and the user input as gen_ai.input.messages (disable with OTEL_INSTRUMENTATION_A2A_CAPTURE_CONTENT=false).

Complementary, not duplicative. a2a-sdk already ships an OTel tracing layer (a2a.utils.telemetry) that decorates its transports and request handlers with generic spans under the instrumenting module a2a-python-sdk — those describe protocol plumbing, carry no gen-ai semconv, and do not wrap the user's execute implementation (which is an abstract method applications override). This package supplies exactly that missing gen-ai agent boundary, mirroring how sibling loongsuite packages layer ARMS gen-ai spans over frameworks that already emit some telemetry.

AgentExecutor.execute is an ABC method overridden by every concrete agent, so the instrumentor (1) walks the existing AgentExecutor subclass tree at instrument() time and wraps each subclass's own execute via wrapt, and (2) installs an __init_subclass__ hook on AgentExecutor so executors defined after instrumentation are wrapped as they are created. A sentinel prevents double-wrapping; uninstrument() unwraps every marked execute and restores __init_subclass__.

Testing

tests/test_instrumentor.py drives the real a2a-sdk AgentExecutor ABC with in-process executor subclasses and asserts on spans exported to an InMemorySpanExporter (no network). Each GREEN assertion is paired with a RED baseline:

  • RED: executing an agent with no instrumentor active produces no AGENT span.
  • GREEN (existing subclass): AGENT span with gen_ai.framework=a2a, invoke_agent, a2a.context_id/a2a.task_id; inner agent work nests under the AGENT span (same trace_id, correct parent); user input captured.
  • GREEN (late subclass): an executor defined after instrument() still gets an AGENT span — proving the __init_subclass__ hook.
  • RED-after-teardown: after uninstrument(), executing a fresh executor produces no AGENT span.
  • Plus exception recording (span status ERROR + re-raise) and idempotent double-uninstrument.

8 tests pass (pytest), on a2a-sdk 1.1.5 + opentelemetry-sdk.

Notes

  • Package layout, pyproject.toml, entry point, CHANGELOG and bootstrap-registry entry follow the existing loongsuite-instrumentation-* conventions (e.g. terminus2).
  • CHANGELOG.md updated (repo requirement).

…rc/opentelemetry/instrumentation/a2a/package.py
…rc/opentelemetry/instrumentation/a2a/version.py
…rc/opentelemetry/instrumentation/a2a/__init__.py
CI precommit failed on the new package: E402 in tests/conftest.py (imports intentionally after sys.path setup + pytest_configure), PLC0415 for lazy imports in src/tests, and I001/format. Follow the existing per-file-ignores convention used by every other loongsuite instrumentation package (PLC0415 for the package, E402+F811 for tests), and apply ruff import sorting + formatting. No behavior change; 8 tests still pass.

Copilot AI 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 review overview

🟡 Changes recommended

Instrumentation lifecycle bugs, default sensitive-content capture, and missing CI registration must be resolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 High severity · 4 Medium severity

Open (6)
What changed in this PR

Adds automatic OpenTelemetry instrumentation for A2A server-side agent execution.

Changes:

  • Creates AGENT spans around AgentExecutor.execute.
  • Supports existing and late-defined executor subclasses.
  • Adds package metadata, tests, documentation, and bootstrap registration.
File Description
pyproject.toml Adds lint exceptions.
loongsuite-distro/​.../​loongsuite_instrumentation_a2a.py Registers bootstrap metadata.
.../​tests/​test_instrumentor.py Tests spans and lifecycle behavior.
.../​tests/​conftest.py Configures tracing fixtures.
.../​tests/​__init__.py Initializes the test package.
.../​test-requirements.txt Defines test dependencies.
.../​version.py Defines package version.
.../​package.py Declares instrumented dependency.
.../​a2a/​__init__.py Implements instrumentation.
.../​README.md Documents installation and usage.
.../​pyproject.toml Defines package metadata and entry point.
.../​LICENSE Adds Apache 2.0 license.
.../​CHANGELOG.md Records the initial release.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +93 to +94
def _capture_content() -> bool:
return os.getenv(_CAPTURE_CONTENT_ENV, "true").lower() != "false"
Comment on lines +285 to +293
try:
del base.__dict__["__init_subclass__"]
except (KeyError, TypeError):
try:
base.__init_subclass__ = classmethod(
lambda cls, **kw: None
)
except Exception: # pragma: no cover - defensive
pass
build-backend = "hatchling.build"

[project]
name = "loongsuite-instrumentation-a2a"
)

with self._tracer.start_as_current_span(
f"{_OP_INVOKE_AGENT} {_AGENT_NAME}",
Comment on lines +229 to +234
def __init__(self):
super().__init__()
self._wrapper = None
self._base = None
self._saved_init_subclass = None
self._wrapped_classes = []
Comment on lines +261 to +266
try:
if saved is not None:
saved.__func__(cls, **kw)
except Exception: # pragma: no cover - defensive
pass
_wrap_execute(cls, wrapper)

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] Add instrumentation for official A2A

5 participants