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
Original file line number Diff line number Diff line change
Expand Up @@ -77,25 +77,26 @@ def __init__(
user_details: Optional human user details
span_details: Optional span configuration (parent context, timing, kind)
"""
kind = SpanKind.INTERNAL
parent_context = None
start_time = None
end_time = None
if span_details is not None:
if span_details.span_kind is not None:
kind = span_details.span_kind
parent_context = span_details.parent_context
start_time = span_details.start_time
end_time = span_details.end_time
# spanKind defaults to INTERNAL; allow override via span_details
resolved_span_details = (
SpanDetails(
span_kind=span_details.span_kind
if span_details and span_details.span_kind
else SpanKind.INTERNAL,
parent_context=span_details.parent_context if span_details else None,
start_time=span_details.start_time if span_details else None,
end_time=span_details.end_time if span_details else None,
span_links=span_details.span_links if span_details else None,
)
if span_details
else SpanDetails(span_kind=SpanKind.INTERNAL)
)

super().__init__(
kind=kind,
operation_name=EXECUTE_TOOL_OPERATION_NAME,
activity_name=f"{EXECUTE_TOOL_OPERATION_NAME} {details.tool_name}",
agent_details=agent_details,
parent_context=parent_context,
start_time=start_time,
end_time=end_time,
span_details=resolved_span_details,
)

# Extract details
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from typing import List

from opentelemetry.trace import SpanKind

from .agent_details import AgentDetails
from .constants import (
CHANNEL_LINK_KEY,
Expand Down Expand Up @@ -74,22 +76,24 @@ def __init__(
user_details: Optional human user details
span_details: Optional span configuration (parent context, timing)
"""
parent_context = None
start_time = None
end_time = None
if span_details is not None:
parent_context = span_details.parent_context
start_time = span_details.start_time
end_time = span_details.end_time
# spanKind for InferenceScope is always CLIENT
resolved_span_details = (
SpanDetails(
span_kind=SpanKind.CLIENT,
parent_context=span_details.parent_context if span_details else None,
start_time=span_details.start_time if span_details else None,
end_time=span_details.end_time if span_details else None,
span_links=span_details.span_links if span_details else None,
)
if span_details
else SpanDetails(span_kind=SpanKind.CLIENT)
)

super().__init__(
kind="Client",
operation_name=details.operationName.value,
activity_name=f"{details.operationName.value} {details.model}",
agent_details=agent_details,
parent_context=parent_context,
start_time=start_time,
end_time=end_time,
span_details=resolved_span_details,
)

if request.content:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,25 +93,26 @@ def __init__(
if agent_details.agent_name:
activity_name = f"{INVOKE_AGENT_OPERATION_NAME} {agent_details.agent_name}"

kind = SpanKind.CLIENT
parent_context = None
start_time = None
end_time = None
if span_details is not None:
if span_details.span_kind is not None:
kind = span_details.span_kind
parent_context = span_details.parent_context
start_time = span_details.start_time
end_time = span_details.end_time
# spanKind defaults to CLIENT; allow override via span_details
resolved_span_details = (
SpanDetails(
span_kind=span_details.span_kind
if span_details and span_details.span_kind
else SpanKind.CLIENT,
parent_context=span_details.parent_context if span_details else None,
start_time=span_details.start_time if span_details else None,
end_time=span_details.end_time if span_details else None,
span_links=span_details.span_links if span_details else None,
)
if span_details
else SpanDetails(span_kind=SpanKind.CLIENT)
)

super().__init__(
kind=kind,
operation_name=INVOKE_AGENT_OPERATION_NAME,
activity_name=activity_name,
agent_details=agent_details,
parent_context=parent_context,
start_time=start_time,
end_time=end_time,
span_details=resolved_span_details,
)

self.set_tag_maybe(SESSION_ID_KEY, request.session_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
GEN_AI_CALLER_CLIENT_IP_KEY,
GEN_AI_CONVERSATION_ID_KEY,
GEN_AI_CONVERSATION_ITEM_LINK_KEY,
SERVER_ADDRESS_KEY,
SERVER_PORT_KEY,
SERVICE_NAME_KEY,
SESSION_DESCRIPTION_KEY,
SESSION_ID_KEY,
Expand Down Expand Up @@ -167,6 +169,21 @@ def user_client_ip(self, value: str | None) -> "BaggageBuilder":
self._set(GEN_AI_CALLER_CLIENT_IP_KEY, validate_and_normalize_ip(value))
return self

def invoke_agent_server(self, address: str | None, port: int | None = None) -> "BaggageBuilder":
"""Set the invoke agent server address and port baggage values.

Args:
address: The server address (hostname) of the target agent service.
port: Optional server port. Only recorded when different from 443.

Returns:
Self for method chaining
"""
self._set(SERVER_ADDRESS_KEY, address)
if port is not None and port != 443:
self._set(SERVER_PORT_KEY, str(port))
return self

def conversation_id(self, value: str | None) -> "BaggageBuilder":
"""Set the conversation ID baggage value."""
self._set(GEN_AI_CONVERSATION_ID_KEY, value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

if TYPE_CHECKING:
from .agent_details import AgentDetails
from .span_details import SpanDetails

# Create logger for this module - inherits from 'microsoft_agents_a365.observability.core'
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -93,32 +94,38 @@ def _datetime_to_ns(dt: datetime | None) -> int | None:

def __init__(
self,
kind: "str | SpanKind",
operation_name: str,
activity_name: str,
agent_details: "AgentDetails | None" = None,
parent_context: Context | None = None,
start_time: datetime | None = None,
end_time: datetime | None = None,
span_details: "SpanDetails | None" = None,
):
"""Initialize the OpenTelemetry scope.

Args:
kind: The kind of activity. Accepts a string (e.g. ``"Client"``,
``"Server"``, ``"Internal"``) or an ``opentelemetry.trace.SpanKind``
enum value directly.
operation_name: The name of the operation being traced
activity_name: The name of the activity for display purposes
agent_details: Optional agent details
parent_context: Optional OpenTelemetry Context used to link this span to an
upstream operation. Use ``extract_context_from_headers()`` to extract a
Context from HTTP headers containing W3C traceparent.
start_time: Optional explicit start time as a datetime object.
Useful when recording an operation after it has already completed.
end_time: Optional explicit end time as a datetime object.
When provided, the span will use this timestamp when disposed
instead of the current wall-clock time.
span_details: Optional span configuration including parent context,
start/end times, span kind, and span links. Subclasses may override
``span_details.span_kind`` before calling this constructor;
defaults to ``SpanKind.CLIENT``.
"""
parent_context = span_details.parent_context if span_details else None
start_time = span_details.start_time if span_details else None
end_time = span_details.end_time if span_details else None
span_links = span_details.span_links if span_details else None
kind = (
span_details.span_kind if span_details and span_details.span_kind else SpanKind.CLIENT
)
if not isinstance(kind, SpanKind):
logger.warning(
"span_details.span_kind has invalid type %s (value: %r); "
"falling back to SpanKind.CLIENT",
type(kind).__name__,
kind,
)
kind = SpanKind.CLIENT

self._span: Span | None = None
self._custom_start_time: datetime | None = start_time
self._custom_end_time: datetime | None = end_time
Expand All @@ -130,20 +137,7 @@ def __init__(
if self._is_telemetry_enabled():
tracer = self._get_tracer()

# Resolve activity_kind from either a SpanKind enum or a string
if isinstance(kind, SpanKind):
activity_kind = kind
else:
# Map string kind to SpanKind enum
activity_kind = SpanKind.INTERNAL
if kind.lower() == "client":
activity_kind = SpanKind.CLIENT
elif kind.lower() == "server":
activity_kind = SpanKind.SERVER
elif kind.lower() == "producer":
activity_kind = SpanKind.PRODUCER
elif kind.lower() == "consumer":
activity_kind = SpanKind.CONSUMER
activity_kind = kind

# Get context for parent relationship
# If parent_context is provided, use it directly
Expand All @@ -158,6 +152,7 @@ def __init__(
kind=activity_kind,
context=span_context,
start_time=otel_start_time,
links=span_links,
)

# Log span creation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime

from opentelemetry.context import Context
from opentelemetry.trace import SpanKind
from opentelemetry.trace import Link, SpanKind


@dataclass
Expand All @@ -23,3 +23,6 @@ class SpanDetails:

end_time: datetime | None = None
"""Optional explicit end time as a datetime object."""

span_links: list[Link] | None = None
"""Optional span links to associate with this span for causal relationships."""
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from opentelemetry.trace import SpanKind

from ..agent_details import AgentDetails
from ..constants import (
GEN_AI_CALLER_CLIENT_IP_KEY,
Expand Down Expand Up @@ -64,22 +66,24 @@ def __init__(
user_details: Optional human user details
span_details: Optional span configuration (parent context, timing)
"""
parent_context = None
start_time = None
end_time = None
if span_details is not None:
parent_context = span_details.parent_context
start_time = span_details.start_time
end_time = span_details.end_time
# spanKind for OutputScope is always CLIENT
resolved_span_details = (
SpanDetails(
span_kind=SpanKind.CLIENT,
parent_context=span_details.parent_context if span_details else None,
start_time=span_details.start_time if span_details else None,
end_time=span_details.end_time if span_details else None,
span_links=span_details.span_links if span_details else None,
)
if span_details
else SpanDetails(span_kind=SpanKind.CLIENT)
)

super().__init__(
kind="Client",
operation_name=OUTPUT_OPERATION_NAME,
activity_name=(f"{OUTPUT_OPERATION_NAME} {agent_details.agent_id}"),
agent_details=agent_details,
parent_context=parent_context,
start_time=start_time,
end_time=end_time,
span_details=resolved_span_details,
)

self.set_tag_maybe(GEN_AI_CONVERSATION_ID_KEY, request.conversation_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,24 @@
# Channel dimensions
consts.CHANNEL_NAME_KEY, # microsoft.channel.name
consts.CHANNEL_LINK_KEY, # microsoft.channel.link
# User / Caller attributes
consts.USER_ID_KEY, # user.id
consts.USER_NAME_KEY, # user.name
consts.USER_EMAIL_KEY, # user.email
# Service attributes
consts.SERVICE_NAME_KEY, # service.name
]

# Invoke Agent–specific attributes
INVOKE_AGENT_ATTRIBUTES = [
# Caller / Invoker attributes
consts.USER_ID_KEY, # user.id
consts.USER_NAME_KEY, # user.name
consts.USER_EMAIL_KEY, # user.email
# Caller Agent (A2A) attributes
consts.GEN_AI_CALLER_AGENT_ID_KEY, # microsoft.a365.caller.agent.id
consts.GEN_AI_CALLER_AGENT_NAME_KEY, # microsoft.a365.caller.agent.name
consts.GEN_AI_CALLER_AGENT_USER_ID_KEY, # microsoft.a365.caller.agent.user.id
consts.GEN_AI_CALLER_AGENT_EMAIL_KEY, # microsoft.a365.caller.agent.user.email
consts.GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY, # microsoft.a365.caller.agent.blueprint.id
consts.GEN_AI_CALLER_AGENT_PLATFORM_ID_KEY, # microsoft.a365.caller.agent.platform.id
# Server address/port for invoke agent target
consts.SERVER_ADDRESS_KEY, # server.address
consts.SERVER_PORT_KEY, # server.port
]
35 changes: 35 additions & 0 deletions tests/observability/core/test_baggage_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
GEN_AI_AGENT_EMAIL_KEY,
GEN_AI_AGENT_ID_KEY,
GEN_AI_CALLER_CLIENT_IP_KEY,
SERVER_ADDRESS_KEY,
SERVER_PORT_KEY,
SERVICE_NAME_KEY,
SESSION_DESCRIPTION_KEY,
SESSION_ID_KEY,
Expand Down Expand Up @@ -329,6 +331,39 @@ def test_operation_source_method(self):
current_baggage = baggage.get_all()
self.assertIsNone(current_baggage.get(SERVICE_NAME_KEY))

def test_invoke_agent_server_sets_address_and_port(self):
"""Test that invoke_agent_server sets both address and non-443 port."""
address = "app.azurewebsites.net"
port = 8080

with BaggageBuilder().invoke_agent_server(address, port).build():
current_baggage = baggage.get_all()
self.assertEqual(current_baggage.get(SERVER_ADDRESS_KEY), address)
self.assertEqual(current_baggage.get(SERVER_PORT_KEY), str(port))

# After scope exit, baggage should be cleared
current_baggage = baggage.get_all()
self.assertIsNone(current_baggage.get(SERVER_ADDRESS_KEY))
self.assertIsNone(current_baggage.get(SERVER_PORT_KEY))

def test_invoke_agent_server_omits_port_when_443(self):
"""Test that invoke_agent_server omits port when it is the default 443."""
address = "app.azurewebsites.net"

with BaggageBuilder().invoke_agent_server(address, 443).build():
current_baggage = baggage.get_all()
self.assertEqual(current_baggage.get(SERVER_ADDRESS_KEY), address)
self.assertIsNone(current_baggage.get(SERVER_PORT_KEY))

def test_invoke_agent_server_sets_address_only_when_port_none(self):
"""Test that invoke_agent_server sets only address when port is None."""
address = "app.azurewebsites.net"

with BaggageBuilder().invoke_agent_server(address).build():
current_baggage = baggage.get_all()
self.assertEqual(current_baggage.get(SERVER_ADDRESS_KEY), address)
self.assertIsNone(current_baggage.get(SERVER_PORT_KEY))


if __name__ == "__main__":
unittest.main()
Loading
Loading