Skip to content
Merged
7 changes: 2 additions & 5 deletions splunk-ao-a2a/src/splunk_ao_a2a/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
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)
# A2A protocol span attributes expected by the ingest API
A2A_TASK_ID = "a2a.task.id"
A2A_CONTEXT_ID = "a2a.context_id"
A2A_RPC_METHOD = "a2a.rpc.method"
Expand All @@ -19,7 +19,7 @@
# OTel GenAI semantic convention attributes — span type determination
GENAI_OPERATION_NAME = "gen_ai.operation.name"
GENAI_AGENT_NAME = "gen_ai.agent.name"
GENAI_SYSTEM = "gen_ai.system"
GENAI_CONVERSATION_ID = "gen_ai.conversation.id"
GENAI_TOOL_NAME = "gen_ai.tool.name"

# OTel GenAI semantic convention attributes — input/output content
Expand All @@ -36,9 +36,6 @@
# Finish reasons
FINISH_REASON_STOP = "stop"

# Session correlation
SESSION_ID = "session.id"

# Span link attributes for cross-agent correlation
LINK_TYPE = "link.type"
LINK_TYPE_AGENT_HANDOFF = "agent_handoff"
Expand Down
2 changes: 1 addition & 1 deletion splunk-ao-a2a/src/splunk_ao_a2a/_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@

from splunk_ao_a2a._constants import (
AGNTCY_OBSERVE_KEY,
SPLUNK_AO_OBSERVE_KEY,
LINK_FROM_AGENT,
LINK_TYPE,
LINK_TYPE_AGENT_HANDOFF,
SPLUNK_AO_OBSERVE_KEY,
)

_logger = logging.getLogger(__name__)
Expand Down
16 changes: 6 additions & 10 deletions splunk-ao-a2a/src/splunk_ao_a2a/_spans.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@
ERROR_STATES,
FINISH_REASON_STOP,
GENAI_AGENT_NAME,
GENAI_CONVERSATION_ID,
GENAI_INPUT_MESSAGES,
GENAI_OPERATION_NAME,
GENAI_OUTPUT_MESSAGES,
GENAI_RESPONSE_FINISH_REASONS,
GENAI_SYSTEM,
GENAI_TOOL_NAME,
ROLE_ASSISTANT,
ROLE_USER,
SESSION_ID,
)


Expand Down Expand Up @@ -68,15 +67,14 @@ def set_client_attributes(
) -> None:
"""Set standard A2A and GenAI attributes on a client span."""
span.set_attribute(GENAI_OPERATION_NAME, "invoke_agent")
span.set_attribute(GENAI_SYSTEM, "a2a")
span.set_attribute(A2A_RPC_METHOD, rpc_method)

if agent_name:
span.set_attribute(GENAI_AGENT_NAME, agent_name)

if hasattr(request, "context_id") and request.context_id:
span.set_attribute(A2A_CONTEXT_ID, str(request.context_id))
span.set_attribute(SESSION_ID, str(request.context_id))
span.set_attribute(GENAI_CONVERSATION_ID, str(request.context_id))

if hasattr(request, "task_id") and request.task_id:
span.set_attribute(A2A_TASK_ID, str(request.task_id))
Expand All @@ -90,7 +88,6 @@ def set_server_attributes(
) -> None:
"""Set standard A2A and GenAI attributes on a server span."""
span.set_attribute(GENAI_OPERATION_NAME, "invoke_agent")
span.set_attribute(GENAI_SYSTEM, "a2a")
span.set_attribute(A2A_RPC_METHOD, rpc_method)

if agent_name:
Expand All @@ -101,7 +98,7 @@ def set_server_attributes(
context_id = getattr(message, "context_id", None)
if context_id:
span.set_attribute(A2A_CONTEXT_ID, str(context_id))
span.set_attribute(SESSION_ID, str(context_id))
span.set_attribute(GENAI_CONVERSATION_ID, str(context_id))

task_id = getattr(message, "task_id", None)
if task_id:
Expand All @@ -111,7 +108,6 @@ def set_server_attributes(
def set_tool_attributes(span: trace.Span, rpc_method: str) -> None:
"""Set attributes for tool-like A2A operations (get_task, cancel_task, get_card)."""
span.set_attribute(GENAI_OPERATION_NAME, "execute_tool")
span.set_attribute(GENAI_SYSTEM, "a2a")
span.set_attribute(A2A_RPC_METHOD, rpc_method)
span.set_attribute(GENAI_TOOL_NAME, rpc_method)

Expand Down Expand Up @@ -180,7 +176,7 @@ def set_simple_input(span: trace.Span, args: tuple, rpc_method: str) -> None:
def track_task_state(span: trace.Span, obj: Any) -> None:
"""Record A2A task state and ID on *span*.

Sets ``a2a.task.state``, ``a2a.task.id``, and ``gen_ai.response.finish_reasons``.
Sets the A2A task state and ID plus ``gen_ai.response.finish_reasons``.
Marks the span as an error when the task enters a terminal error state.
"""
if obj is None:
Expand All @@ -195,9 +191,9 @@ def track_task_state(span: trace.Span, obj: Any) -> None:

if state_value in ERROR_STATES:
span.set_status(StatusCode.ERROR, f"A2A task {state_value}")
span.set_attribute(GENAI_RESPONSE_FINISH_REASONS, json.dumps([state_value]))
span.set_attribute(GENAI_RESPONSE_FINISH_REASONS, (state_value,))
elif state_value == "completed":
span.set_attribute(GENAI_RESPONSE_FINISH_REASONS, json.dumps([FINISH_REASON_STOP]))
span.set_attribute(GENAI_RESPONSE_FINISH_REASONS, (FINISH_REASON_STOP,))

task_id = getattr(obj, "id", None)
if task_id:
Expand Down
11 changes: 5 additions & 6 deletions splunk-ao-a2a/tests/test_spans.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@
A2A_TASK_ID,
A2A_TASK_STATE,
GENAI_AGENT_NAME,
GENAI_CONVERSATION_ID,
GENAI_INPUT_MESSAGES,
GENAI_OPERATION_NAME,
GENAI_OUTPUT_MESSAGES,
GENAI_RESPONSE_FINISH_REASONS,
GENAI_SYSTEM,
GENAI_TOOL_NAME,
SESSION_ID,
)


Expand Down Expand Up @@ -55,12 +54,12 @@ def test_sets_standard_attributes(self, span):

# Then: all expected attributes are set
span.set_attribute.assert_any_call(GENAI_OPERATION_NAME, "invoke_agent")
span.set_attribute.assert_any_call(GENAI_SYSTEM, "a2a")
span.set_attribute.assert_any_call(A2A_RPC_METHOD, "SendMessage")
span.set_attribute.assert_any_call(GENAI_AGENT_NAME, "my-agent")
span.set_attribute.assert_any_call(A2A_CONTEXT_ID, "ctx-1")
span.set_attribute.assert_any_call(SESSION_ID, "ctx-1")
span.set_attribute.assert_any_call(GENAI_CONVERSATION_ID, "ctx-1")
span.set_attribute.assert_any_call(A2A_TASK_ID, "task-1")
assert "gen_ai.system" not in [call.args[0] for call in span.set_attribute.call_args_list]

def test_skips_agent_name_when_none(self, span):
# Given: no agent name
Expand All @@ -84,7 +83,7 @@ def test_sets_attributes_from_message(self, span):

# Then: attributes extracted from message
span.set_attribute.assert_any_call(A2A_CONTEXT_ID, "ctx-2")
span.set_attribute.assert_any_call(SESSION_ID, "ctx-2")
span.set_attribute.assert_any_call(GENAI_CONVERSATION_ID, "ctx-2")
span.set_attribute.assert_any_call(A2A_TASK_ID, "task-2")


Expand Down Expand Up @@ -183,7 +182,7 @@ def test_sets_completed_state(self, span):

# Then: state and finish reason set
span.set_attribute.assert_any_call(A2A_TASK_STATE, "completed")
span.set_attribute.assert_any_call(GENAI_RESPONSE_FINISH_REASONS, json.dumps(["stop"]))
span.set_attribute.assert_any_call(GENAI_RESPONSE_FINISH_REASONS, ("stop",))
span.set_attribute.assert_any_call(A2A_TASK_ID, "t-1")

@pytest.mark.parametrize("state", ["failed", "rejected", "canceled"])
Expand Down
8 changes: 7 additions & 1 deletion splunk-ao-a2a/tests/test_splunk_ao_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,14 @@ def exporter_factory(**kwargs: object) -> RecordingExporter:
}
exported = delegate.spans[0]
assert exported.instrumentation_scope.name == INSTRUMENTOR_NAME
assert exported.attributes["gen_ai.system"] == "a2a"
assert "gen_ai.system" not in exported.attributes
assert exported.attributes["splunk_ao.system"] == "splunk_ao_python"
assert exported.attributes["a2a.rpc.method"] == "SendMessage"
assert "splunk_ao.a2a.rpc.method" not in exported.attributes
assert exported.attributes["gen_ai.conversation.id"] == "context-id"
assert exported.attributes["splunk_ao.session.id"] == "context-id"
assert exported.attributes["gen_ai.operation.name"] == "invoke_agent"
assert exported.attributes["splunk_ao.operation.name"] == "invoke_agent"
assert exported.resource.attributes["splunk_ao.project.name"] == "a2a-project"
assert exported.resource.attributes["splunk_ao.logstream.name"] == "a2a-agent-stream"
assert "splunk_ao.project.name" not in exported.attributes
15 changes: 15 additions & 0 deletions src/splunk_ao/converter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Conversion helpers shared by Splunk AO telemetry paths."""

from splunk_ao.converter.attribute_mapping import (
CONTENT_ALIAS_BY_GEN_AI,
SPLUNK_ALIAS_BY_GEN_AI,
build_span_attributes,
normalize_attributes_for_export,
)

__all__ = [
"CONTENT_ALIAS_BY_GEN_AI",
"SPLUNK_ALIAS_BY_GEN_AI",
"build_span_attributes",
"normalize_attributes_for_export",
]
Loading
Loading