Skip to content
Closed
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
33 changes: 17 additions & 16 deletions galileo-a2a/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Python versions](https://img.shields.io/pypi/pyversions/galileo-a2a.svg)](https://pypi.org/project/galileo-a2a/)
[![License](https://img.shields.io/pypi/l/galileo-a2a.svg)](https://github.com/rungalileo/galileo-python/blob/main/LICENSE)

Galileo observability for [A2A (Agent-to-Agent)](https://github.com/google/A2A) protocol interactions. Automatic tracing of agent-to-agent calls, task lifecycle, and cross-agent distributed trace correlation.
Splunk AO observability for [A2A (Agent-to-Agent)](https://github.com/google/A2A) protocol interactions. Automatic tracing of agent-to-agent calls, task lifecycle, and cross-agent distributed trace correlation.

## How It Works

Expand All @@ -25,31 +25,31 @@ Galileo observability for [A2A (Agent-to-Agent)](https://github.com/google/A2A)
│ └─────────────────┬────────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Galileo │ │
│ │ Splunk AO │ │
│ │ (Trace Explorer) │ │
│ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```

`galileo-a2a` instruments both the **client** (outbound calls) and **server** (inbound requests) sides of the A2A protocol. Trace context is propagated through A2A message metadata so all agents appear in a single distributed trace in Galileo.
`galileo-a2a` instruments both the **client** (outbound calls) and **server** (inbound requests) sides of the A2A protocol. Trace context is propagated through A2A message metadata so all agents appear in a single distributed trace in Splunk AO.

## Installation

```bash
pip install galileo-a2a
```

**Requirements:** Python 3.10+, a [Galileo API key](https://www.rungalileo.io/), and [a2a-sdk](https://pypi.org/project/a2a-sdk/) 0.3+
**Requirements:** Python 3.10+, a [Splunk AO API key](https://www.splunk.com/), and [a2a-sdk](https://pypi.org/project/a2a-sdk/) 0.3+

## Quick Start

```python
from splunk_ao.otel import GalileoSpanProcessor, add_galileo_span_processor
from splunk_ao.otel import SplunkAOSpanProcessor, add_galileo_span_processor
from galileo_a2a import A2AInstrumentor
from opentelemetry.sdk.trace import TracerProvider

provider = TracerProvider()
add_galileo_span_processor(provider, GalileoSpanProcessor())
add_galileo_span_processor(provider, SplunkAOSpanProcessor())
A2AInstrumentor().instrument(tracer_provider=provider, agent_name="orchestrator")
```

Expand All @@ -63,13 +63,14 @@ Once instrumented, all `a2a-sdk` client and server interactions produce OTel spa
| `agent_name` | Name of this agent, set on spans as `gen_ai.agent.name`. |
| `capture_content` | Set to `False` to disable capturing message content (e.g. for PII compliance). |

Environment variables for the Galileo exporter:
Environment variables for the Splunk AO exporter:

| Environment Variable | Description |
|---------------------|-------------|
| `GALILEO_API_KEY` | Galileo API key (required) |
| `GALILEO_PROJECT` | Project name (alternative to `GalileoSpanProcessor(project=...)`) |
| `GALILEO_LOG_STREAM` | Log stream name (alternative to `GalileoSpanProcessor(logstream=...)`) |
| `SPLUNK_AO_API_KEY` | Splunk AO API key (required) |
| `SPLUNK_AO_API_URL` | Splunk AO API host URL (required for self-hosted deployments, e.g. `https://agent-observability-api.rc0.signalfx.com`) |
| `SPLUNK_AO_PROJECT` | Project name (alternative to `SplunkAOSpanProcessor(project=...)`) |
| `SPLUNK_AO_LOG_STREAM` | Log stream name (alternative to `SplunkAOSpanProcessor(logstream=...)`) |

## Features

Expand All @@ -83,11 +84,11 @@ The instrumentor patches both sides of the A2A protocol:

### Cross-Agent Distributed Tracing

When Agent A calls Agent B, trace context is propagated through A2A message metadata. The receiving agent joins the caller's trace, so both agents appear in a single distributed trace in Galileo.
When Agent A calls Agent B, trace context is propagated through A2A message metadata. The receiving agent joins the caller's trace, so both agents appear in a single distributed trace in Splunk AO.

### Session Tracking

A2A's `context_id` is mapped to `session.id`, grouping all interactions within the same conversation into a Galileo session.
A2A's `context_id` is mapped to `session.id`, grouping all interactions within the same conversation into a Splunk AO session.

### Disabling Instrumentation

Expand Down Expand Up @@ -119,7 +120,7 @@ from a2a.types import (
AgentCapabilities, AgentCard, AgentSkill, Message, Role,
TaskState, TaskStatus, TaskStatusUpdateEvent, TextPart,
)
from splunk_ao.otel import GalileoSpanProcessor, add_galileo_span_processor
from splunk_ao.otel import SplunkAOSpanProcessor, add_galileo_span_processor
from galileo_a2a import A2AInstrumentor
from langchain.agents import create_agent
from langchain_core.tools import tool
Expand All @@ -132,7 +133,7 @@ from typing_extensions import TypedDict

# ---- Only 4 lines needed for full distributed tracing ----
provider = TracerProvider()
add_galileo_span_processor(provider, GalileoSpanProcessor())
add_galileo_span_processor(provider, SplunkAOSpanProcessor())
A2AInstrumentor().instrument(tracer_provider=provider, agent_name="orchestrator")
LangchainInstrumentor().instrument(tracer_provider=provider)

Expand Down Expand Up @@ -255,13 +256,13 @@ async def main():
provider.shutdown()

if __name__ == "__main__":
# Set environment variables: GALILEO_API_KEY, OPENAI_API_KEY
# Set environment variables: SPLUNK_AO_API_KEY, OPENAI_API_KEY
asyncio.run(main())
```

## Resources

- [Galileo Documentation](https://v2docs.galileo.ai)
- [Splunk AO Documentation](https://docs.splunk.com/)
- [A2A Protocol Specification](https://a2a-protocol.org)
- [a2a-sdk Documentation](https://pypi.org/project/a2a-sdk)

Expand Down
8 changes: 4 additions & 4 deletions galileo-a2a/examples/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
GALILEO_API_KEY=your-galileo-key
GALILEO_CONSOLE_URL=http://localhost:8088
GALILEO_PROJECT=a2a-distributed-tracing-demo
GALILEO_LOG_STREAM=dev
SPLUNK_AO_API_KEY=your-splunk-ao-key
SPLUNK_AO_API_URL=https://<splunk-ao-api-host>
SPLUNK_AO_PROJECT=a2a-distributed-tracing-demo
SPLUNK_AO_LOG_STREAM=dev
OPENAI_API_KEY=your-openai-key
8 changes: 4 additions & 4 deletions galileo-a2a/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
env = [
"GALILEO_CONSOLE_URL=http://fake.test:8088",
"GALILEO_API_KEY=api-1234567890",
"GALILEO_PROJECT=test-project",
"GALILEO_LOG_STREAM=test-log-stream",
"SPLUNK_AO_CONSOLE_URL=http://fake.test:8088",
"SPLUNK_AO_API_KEY=api-1234567890",
"SPLUNK_AO_PROJECT=test-project",
"SPLUNK_AO_LOG_STREAM=test-log-stream",
]
addopts = [
"-v",
Expand Down
4 changes: 2 additions & 2 deletions galileo-a2a/src/galileo_a2a/_client_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from opentelemetry import trace
from opentelemetry.trace import StatusCode, Tracer

from galileo_a2a._constants import GALILEO_OBSERVE_KEY
from galileo_a2a._constants import SPLUNK_AO_OBSERVE_KEY
from galileo_a2a._context import inject_trace_context, iter_with_context
from galileo_a2a._spans import (
set_client_attributes,
Expand Down Expand Up @@ -107,7 +107,7 @@ async def wrapper(
otel_context.detach(token)

request_metadata = dict(request_metadata) if request_metadata else {}
request_metadata[GALILEO_OBSERVE_KEY] = observe_ctx
request_metadata[SPLUNK_AO_OBSERVE_KEY] = observe_ctx

result = original(
self,
Expand Down
4 changes: 2 additions & 2 deletions galileo-a2a/src/galileo_a2a/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
# Instrumentor identity
from galileo_a2a._version import __version__

INSTRUMENTOR_NAME = "galileo-a2a"
INSTRUMENTOR_NAME = "splunk-ao-a2a"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We update the INSTRUMENTOR_NAME to splunk-ao-a2a but here we still use galileo-a2a. How will it affect the release and package name?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — pyproject.toml now has name = "splunk-ao-a2a". INSTRUMENTOR_NAME was already updated to "splunk-ao-a2a" in the previous commit. Both are now consistent.

INSTRUMENTOR_VERSION = __version__

# Metadata keys for in-band trace context propagation
GALILEO_OBSERVE_KEY = "galileo_observe"
SPLUNK_AO_OBSERVE_KEY = "splunk_ao_observe"
AGNTCY_OBSERVE_KEY = "observe" # compatibility with AGNTCY Observe SDK

# A2A span attribute keys (match API-side A2A extension expectations)
Expand Down
6 changes: 3 additions & 3 deletions galileo-a2a/src/galileo_a2a/_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from galileo_a2a._constants import (
AGNTCY_OBSERVE_KEY,
GALILEO_OBSERVE_KEY,
SPLUNK_AO_OBSERVE_KEY,
LINK_FROM_AGENT,
LINK_TYPE,
LINK_TYPE_AGENT_HANDOFF,
Expand Down Expand Up @@ -46,14 +46,14 @@ def inject_trace_context(agent_name: str | None = None) -> dict[str, str]:
def extract_trace_context(metadata: dict[str, Any] | None) -> dict[str, str] | None:
"""Extract trace context from A2A request metadata.

Checks the Galileo key first, then falls back to the AGNTCY key for
Checks the Splunk AO key first, then falls back to the AGNTCY key for
interoperability with the AGNTCY Observe SDK. Returns ``None`` when
no trace context is present.
"""
if not metadata or not isinstance(metadata, dict):
return None

ctx = metadata.get(GALILEO_OBSERVE_KEY)
ctx = metadata.get(SPLUNK_AO_OBSERVE_KEY)
if ctx and isinstance(ctx, dict):
return {str(k): str(v) for k, v in ctx.items()}

Expand Down
8 changes: 4 additions & 4 deletions galileo-a2a/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
# 3. Security - prevents real API keys from leaking into test logs
import os

os.environ["GALILEO_CONSOLE_URL"] = "http://fake.test:8088"
os.environ["GALILEO_API_KEY"] = "api-1234567890"
os.environ["GALILEO_PROJECT"] = "test-project"
os.environ["GALILEO_LOG_STREAM"] = "test-log-stream"
os.environ["SPLUNK_AO_CONSOLE_URL"] = "http://fake.test:8088"
os.environ["SPLUNK_AO_API_KEY"] = "api-1234567890"
os.environ["SPLUNK_AO_PROJECT"] = "test-project"
os.environ["SPLUNK_AO_LOG_STREAM"] = "test-log-stream"
# fmt: on
18 changes: 9 additions & 9 deletions galileo-a2a/tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.trace import Link

from galileo_a2a._constants import AGNTCY_OBSERVE_KEY, GALILEO_OBSERVE_KEY
from galileo_a2a._constants import AGNTCY_OBSERVE_KEY, SPLUNK_AO_OBSERVE_KEY
from galileo_a2a._context import (
create_span_link_from_context,
extract_trace_context,
Expand Down Expand Up @@ -33,10 +33,10 @@ def test_includes_agent_name(self):


class TestExtractTraceContext:
def test_extracts_from_galileo_observe_key(self):
# Given: metadata with galileo_observe context
def test_extracts_from_splunk_ao_observe_key(self):
# Given: metadata with splunk_ao_observe context
metadata = {
GALILEO_OBSERVE_KEY: {
SPLUNK_AO_OBSERVE_KEY: {
"traceparent": "00-aabb00112233445566778899aabbccdd-1122334455667788-01",
"agent_name": "planner",
}
Expand All @@ -45,7 +45,7 @@ def test_extracts_from_galileo_observe_key(self):
# When: extracting trace context
result = extract_trace_context(metadata)

# Then: context extracted from galileo_observe key
# Then: context extracted from splunk_ao_observe key
assert result is not None
assert result["traceparent"] == "00-aabb00112233445566778899aabbccdd-1122334455667788-01"
assert result["agent_name"] == "planner"
Expand All @@ -66,19 +66,19 @@ def test_extracts_from_agntcy_observe_key(self):
assert result is not None
assert "traceparent" in result

def test_galileo_key_takes_precedence_over_agntcy(self):
def test_splunk_ao_key_takes_precedence_over_agntcy(self):
# Given: metadata with both keys
metadata = {
GALILEO_OBSERVE_KEY: {"traceparent": "galileo-traceparent"},
SPLUNK_AO_OBSERVE_KEY: {"traceparent": "splunk-ao-traceparent"},
AGNTCY_OBSERVE_KEY: {"traceparent": "agntcy-traceparent"},
}

# When: extracting trace context
result = extract_trace_context(metadata)

# Then: galileo key takes precedence
# Then: splunk_ao key takes precedence
assert result is not None
assert result["traceparent"] == "galileo-traceparent"
assert result["traceparent"] == "splunk-ao-traceparent"

def test_returns_none_when_missing(self):
# Given: metadata with no observe keys
Expand Down
Loading