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
@@ -1,6 +1,8 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from datetime import datetime

from .agent_details import AgentDetails
from .constants import (
EXECUTE_TOOL_OPERATION_NAME,
Expand Down Expand Up @@ -31,6 +33,8 @@ def start(
tenant_details: TenantDetails,
request: Request | None = None,
parent_id: str | None = None,
start_time: datetime | None = None,
end_time: datetime | None = None,
) -> "ExecuteToolScope":
"""Creates and starts a new scope for tool execution tracing.

Expand All @@ -41,11 +45,18 @@ def start(
request: Optional request details for additional context
parent_id: Optional parent Activity ID used to link this span to an upstream
operation
start_time: Optional explicit start time as a datetime object. Useful when
recording a tool call after execution 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.

Returns:
A new ExecuteToolScope instance
"""
return ExecuteToolScope(details, agent_details, tenant_details, request, parent_id)
return ExecuteToolScope(
details, agent_details, tenant_details, request, parent_id, start_time, end_time
)

def __init__(
self,
Expand All @@ -54,6 +65,8 @@ def __init__(
tenant_details: TenantDetails,
request: Request | None = None,
parent_id: str | None = None,
start_time: datetime | None = None,
end_time: datetime | None = None,
):
"""Initialize the tool execution scope.

Expand All @@ -64,6 +77,11 @@ def __init__(
request: Optional request details for additional context
parent_id: Optional parent Activity ID used to link this span to an upstream
operation
start_time: Optional explicit start time as a datetime object. Useful when
recording a tool call after execution 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.
"""
super().__init__(
kind="Internal",
Expand All @@ -72,6 +90,8 @@ def __init__(
agent_details=agent_details,
tenant_details=tenant_details,
parent_id=parent_id,
start_time=start_time,
end_time=end_time,
)

# Extract details using deconstruction-like approach
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from datetime import datetime
from typing import List

from .agent_details import AgentDetails
Expand Down Expand Up @@ -35,6 +36,8 @@ def start(
tenant_details: TenantDetails,
request: Request | None = None,
parent_id: str | None = None,
start_time: datetime | None = None,
end_time: datetime | None = None,
) -> "InferenceScope":
"""Creates and starts a new scope for inference tracing.

Expand All @@ -45,11 +48,15 @@ def start(
request: Optional request details for additional context
parent_id: Optional parent Activity ID used to link this span to an upstream
operation
start_time: Optional explicit start time as a datetime object.
end_time: Optional explicit end time as a datetime object.

Returns:
A new InferenceScope instance
"""
return InferenceScope(details, agent_details, tenant_details, request, parent_id)
return InferenceScope(
details, agent_details, tenant_details, request, parent_id, start_time, end_time
)

def __init__(
self,
Expand All @@ -58,6 +65,8 @@ def __init__(
tenant_details: TenantDetails,
request: Request | None = None,
parent_id: str | None = None,
start_time: datetime | None = None,
end_time: datetime | None = None,
):
"""Initialize the inference scope.

Expand All @@ -68,6 +77,8 @@ def __init__(
request: Optional request details for additional context
parent_id: Optional parent Activity ID used to link this span to an upstream
operation
start_time: Optional explicit start time as a datetime object.
end_time: Optional explicit end time as a datetime object.
"""

super().__init__(
Expand All @@ -77,6 +88,8 @@ def __init__(
agent_details=agent_details,
tenant_details=tenant_details,
parent_id=parent_id,
start_time=start_time,
end_time=end_time,
)

if request:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Invoke agent scope for tracing agent invocation.

import logging
from datetime import datetime

from .agent_details import AgentDetails
from .constants import (
Expand Down Expand Up @@ -50,6 +51,8 @@ def start(
request: Request | None = None,
caller_agent_details: AgentDetails | None = None,
caller_details: CallerDetails | None = None,
start_time: datetime | None = None,
end_time: datetime | None = None,
) -> "InvokeAgentScope":
"""Create and start a new scope for agent invocation tracing.

Expand All @@ -60,12 +63,20 @@ def start(
request: Optional request details for additional context
caller_agent_details: Optional details of the caller agent
caller_details: Optional details of the non-agentic caller
start_time: Optional explicit start time as a datetime object.
end_time: Optional explicit end time as a datetime object.

Returns:
A new InvokeAgentScope instance
"""
return InvokeAgentScope(
invoke_agent_details, tenant_details, request, caller_agent_details, caller_details
invoke_agent_details,
tenant_details,
request,
caller_agent_details,
caller_details,
start_time,
end_time,
)

def __init__(
Expand All @@ -75,6 +86,8 @@ def __init__(
request: Request | None = None,
caller_agent_details: AgentDetails | None = None,
caller_details: CallerDetails | None = None,
start_time: datetime | None = None,
end_time: datetime | None = None,
):
"""Initialize the agent invocation scope.

Expand All @@ -84,6 +97,8 @@ def __init__(
request: Optional request details for additional context
caller_agent_details: Optional details of the caller agent
caller_details: Optional details of the non-agentic caller
start_time: Optional explicit start time as a datetime object.
end_time: Optional explicit end time as a datetime object.
"""
activity_name = INVOKE_AGENT_OPERATION_NAME
if invoke_agent_details.details.agent_name:
Expand All @@ -97,6 +112,8 @@ def __init__(
activity_name=activity_name,
agent_details=invoke_agent_details.details,
tenant_details=tenant_details,
start_time=start_time,
end_time=end_time,
)

endpoint, _, session_id = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import logging
import os
import time
from datetime import datetime
from threading import Lock
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -72,6 +72,20 @@ def _is_telemetry_enabled(cls) -> bool:
enable_observability = os.getenv(ENABLE_A365_OBSERVABILITY, "").lower()
return (env_value or enable_observability) in ("true", "1", "yes", "on")

@staticmethod
def _datetime_to_ns(dt: datetime | None) -> int | None:
"""Convert a datetime to nanoseconds since epoch.

Args:
dt: Python datetime object, or None

Returns:
Nanoseconds since epoch, or None if input is None
"""
if dt is None:
return None
return int(dt.timestamp() * 1_000_000_000)

def __init__(
self,
kind: str,
Expand All @@ -80,6 +94,8 @@ def __init__(
agent_details: "AgentDetails | None" = None,
tenant_details: "TenantDetails | None" = None,
parent_id: str | None = None,
start_time: datetime | None = None,
end_time: datetime | None = None,
):
"""Initialize the OpenTelemetry scope.

Expand All @@ -91,9 +107,15 @@ def __init__(
tenant_details: Optional tenant details
parent_id: Optional parent Activity ID used to link this span to an upstream
operation
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.
"""
self._span: Span | None = None
self._start_time = time.time()
self._custom_start_time: datetime | None = start_time
self._custom_end_time: datetime | None = end_time
self._has_ended = False
self._error_type: str | None = None
self._exception: Exception | None = None
Expand All @@ -119,7 +141,15 @@ def __init__(
parent_context = parse_parent_id_to_context(parent_id)
span_context = parent_context if parent_context else context.get_current()

self._span = tracer.start_span(activity_name, kind=activity_kind, context=span_context)
# Convert custom start time to OTel-compatible format (nanoseconds since epoch)
otel_start_time = self._datetime_to_ns(start_time)

self._span = tracer.start_span(
activity_name,
kind=activity_kind,
context=span_context,
start_time=otel_start_time,
)

# Log span creation
if self._span:
Expand Down Expand Up @@ -230,14 +260,31 @@ def record_attributes(self, attributes: dict[str, Any] | list[tuple[str, Any]])
if key and key.strip():
self._span.set_attribute(key, value)

def set_end_time(self, end_time: datetime) -> None:
"""Set a custom end time for the scope.

When set, dispose() will pass this value to span.end() instead of using
the current wall-clock time. This is useful when the actual end time of
the operation is known before the scope is disposed.

Args:
end_time: The end time as a datetime object.
"""
self._custom_end_time = end_time

def _end(self) -> None:
"""End the span and record metrics."""
if self._span and self._is_telemetry_enabled() and not self._has_ended:
self._has_ended = True
span_id = f"{self._span.context.span_id:016x}" if self._span.context else "unknown"
logger.info(f"Span ended: '{self._span.name}' ({span_id})")

self._span.end()
# Convert custom end time to OTel-compatible format (nanoseconds since epoch)
otel_end_time = self._datetime_to_ns(self._custom_end_time)
if otel_end_time is not None:
self._span.end(end_time=otel_end_time)
else:
self._span.end()

def __enter__(self):
"""Enter the context manager and make span active."""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from datetime import datetime

from ..agent_details import AgentDetails
from ..constants import GEN_AI_OUTPUT_MESSAGES_KEY
from ..models.response import Response
Expand All @@ -20,6 +22,8 @@ def start(
tenant_details: TenantDetails,
response: Response,
parent_id: str | None = None,
start_time: datetime | None = None,
end_time: datetime | None = None,
) -> "OutputScope":
"""Creates and starts a new scope for output tracing.

Expand All @@ -29,18 +33,22 @@ def start(
response: The response details from the agent
parent_id: Optional parent Activity ID used to link this span to an upstream
operation
start_time: Optional explicit start time as a datetime object.
end_time: Optional explicit end time as a datetime object.

Returns:
A new OutputScope instance
"""
return OutputScope(agent_details, tenant_details, response, parent_id)
return OutputScope(agent_details, tenant_details, response, parent_id, start_time, end_time)

def __init__(
self,
agent_details: AgentDetails,
tenant_details: TenantDetails,
response: Response,
parent_id: str | None = None,
start_time: datetime | None = None,
end_time: datetime | None = None,
):
"""Initialize the output scope.

Expand All @@ -50,6 +58,8 @@ def __init__(
response: The response details from the agent
parent_id: Optional parent Activity ID used to link this span to an upstream
operation
start_time: Optional explicit start time as a datetime object.
end_time: Optional explicit end time as a datetime object.
"""
super().__init__(
kind="Client",
Expand All @@ -58,6 +68,8 @@ def __init__(
agent_details=agent_details,
tenant_details=tenant_details,
parent_id=parent_id,
start_time=start_time,
end_time=end_time,
)

# Initialize accumulated messages list
Expand Down
Loading