Skip to content

Commit e6b7bdf

Browse files
authored
Merge branch 'main' into dependabot/uv/werkzeug-3.1.6
2 parents 17bfa81 + 1c44d8c commit e6b7bdf

6 files changed

Lines changed: 294 additions & 8 deletions

File tree

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/execute_tool_scope.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4+
from datetime import datetime
5+
46
from .agent_details import AgentDetails
57
from .constants import (
68
EXECUTE_TOOL_OPERATION_NAME,
@@ -31,6 +33,8 @@ def start(
3133
tenant_details: TenantDetails,
3234
request: Request | None = None,
3335
parent_id: str | None = None,
36+
start_time: datetime | None = None,
37+
end_time: datetime | None = None,
3438
) -> "ExecuteToolScope":
3539
"""Creates and starts a new scope for tool execution tracing.
3640
@@ -41,11 +45,18 @@ def start(
4145
request: Optional request details for additional context
4246
parent_id: Optional parent Activity ID used to link this span to an upstream
4347
operation
48+
start_time: Optional explicit start time as a datetime object. Useful when
49+
recording a tool call after execution has already completed.
50+
end_time: Optional explicit end time as a datetime object. When provided,
51+
the span will use this timestamp when disposed instead of the
52+
current wall-clock time.
4453
4554
Returns:
4655
A new ExecuteToolScope instance
4756
"""
48-
return ExecuteToolScope(details, agent_details, tenant_details, request, parent_id)
57+
return ExecuteToolScope(
58+
details, agent_details, tenant_details, request, parent_id, start_time, end_time
59+
)
4960

5061
def __init__(
5162
self,
@@ -54,6 +65,8 @@ def __init__(
5465
tenant_details: TenantDetails,
5566
request: Request | None = None,
5667
parent_id: str | None = None,
68+
start_time: datetime | None = None,
69+
end_time: datetime | None = None,
5770
):
5871
"""Initialize the tool execution scope.
5972
@@ -64,6 +77,11 @@ def __init__(
6477
request: Optional request details for additional context
6578
parent_id: Optional parent Activity ID used to link this span to an upstream
6679
operation
80+
start_time: Optional explicit start time as a datetime object. Useful when
81+
recording a tool call after execution has already completed.
82+
end_time: Optional explicit end time as a datetime object. When provided,
83+
the span will use this timestamp when disposed instead of the
84+
current wall-clock time.
6785
"""
6886
super().__init__(
6987
kind="Internal",
@@ -72,6 +90,8 @@ def __init__(
7290
agent_details=agent_details,
7391
tenant_details=tenant_details,
7492
parent_id=parent_id,
93+
start_time=start_time,
94+
end_time=end_time,
7595
)
7696

7797
# Extract details using deconstruction-like approach

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/inference_scope.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4+
from datetime import datetime
45
from typing import List
56

67
from .agent_details import AgentDetails
@@ -35,6 +36,8 @@ def start(
3536
tenant_details: TenantDetails,
3637
request: Request | None = None,
3738
parent_id: str | None = None,
39+
start_time: datetime | None = None,
40+
end_time: datetime | None = None,
3841
) -> "InferenceScope":
3942
"""Creates and starts a new scope for inference tracing.
4043
@@ -45,11 +48,15 @@ def start(
4548
request: Optional request details for additional context
4649
parent_id: Optional parent Activity ID used to link this span to an upstream
4750
operation
51+
start_time: Optional explicit start time as a datetime object.
52+
end_time: Optional explicit end time as a datetime object.
4853
4954
Returns:
5055
A new InferenceScope instance
5156
"""
52-
return InferenceScope(details, agent_details, tenant_details, request, parent_id)
57+
return InferenceScope(
58+
details, agent_details, tenant_details, request, parent_id, start_time, end_time
59+
)
5360

5461
def __init__(
5562
self,
@@ -58,6 +65,8 @@ def __init__(
5865
tenant_details: TenantDetails,
5966
request: Request | None = None,
6067
parent_id: str | None = None,
68+
start_time: datetime | None = None,
69+
end_time: datetime | None = None,
6170
):
6271
"""Initialize the inference scope.
6372
@@ -68,6 +77,8 @@ def __init__(
6877
request: Optional request details for additional context
6978
parent_id: Optional parent Activity ID used to link this span to an upstream
7079
operation
80+
start_time: Optional explicit start time as a datetime object.
81+
end_time: Optional explicit end time as a datetime object.
7182
"""
7283

7384
super().__init__(
@@ -77,6 +88,8 @@ def __init__(
7788
agent_details=agent_details,
7889
tenant_details=tenant_details,
7990
parent_id=parent_id,
91+
start_time=start_time,
92+
end_time=end_time,
8093
)
8194

8295
if request:

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/invoke_agent_scope.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Invoke agent scope for tracing agent invocation.
55

66
import logging
7+
from datetime import datetime
78

89
from .agent_details import AgentDetails
910
from .constants import (
@@ -50,6 +51,8 @@ def start(
5051
request: Request | None = None,
5152
caller_agent_details: AgentDetails | None = None,
5253
caller_details: CallerDetails | None = None,
54+
start_time: datetime | None = None,
55+
end_time: datetime | None = None,
5356
) -> "InvokeAgentScope":
5457
"""Create and start a new scope for agent invocation tracing.
5558
@@ -60,12 +63,20 @@ def start(
6063
request: Optional request details for additional context
6164
caller_agent_details: Optional details of the caller agent
6265
caller_details: Optional details of the non-agentic caller
66+
start_time: Optional explicit start time as a datetime object.
67+
end_time: Optional explicit end time as a datetime object.
6368
6469
Returns:
6570
A new InvokeAgentScope instance
6671
"""
6772
return InvokeAgentScope(
68-
invoke_agent_details, tenant_details, request, caller_agent_details, caller_details
73+
invoke_agent_details,
74+
tenant_details,
75+
request,
76+
caller_agent_details,
77+
caller_details,
78+
start_time,
79+
end_time,
6980
)
7081

7182
def __init__(
@@ -75,6 +86,8 @@ def __init__(
7586
request: Request | None = None,
7687
caller_agent_details: AgentDetails | None = None,
7788
caller_details: CallerDetails | None = None,
89+
start_time: datetime | None = None,
90+
end_time: datetime | None = None,
7891
):
7992
"""Initialize the agent invocation scope.
8093
@@ -84,6 +97,8 @@ def __init__(
8497
request: Optional request details for additional context
8598
caller_agent_details: Optional details of the caller agent
8699
caller_details: Optional details of the non-agentic caller
100+
start_time: Optional explicit start time as a datetime object.
101+
end_time: Optional explicit end time as a datetime object.
87102
"""
88103
activity_name = INVOKE_AGENT_OPERATION_NAME
89104
if invoke_agent_details.details.agent_name:
@@ -97,6 +112,8 @@ def __init__(
97112
activity_name=activity_name,
98113
agent_details=invoke_agent_details.details,
99114
tenant_details=tenant_details,
115+
start_time=start_time,
116+
end_time=end_time,
100117
)
101118

102119
endpoint, _, session_id = (

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/opentelemetry_scope.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import logging
77
import os
8-
import time
8+
from datetime import datetime
99
from threading import Lock
1010
from typing import TYPE_CHECKING, Any
1111

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

75+
@staticmethod
76+
def _datetime_to_ns(dt: datetime | None) -> int | None:
77+
"""Convert a datetime to nanoseconds since epoch.
78+
79+
Args:
80+
dt: Python datetime object, or None
81+
82+
Returns:
83+
Nanoseconds since epoch, or None if input is None
84+
"""
85+
if dt is None:
86+
return None
87+
return int(dt.timestamp() * 1_000_000_000)
88+
7589
def __init__(
7690
self,
7791
kind: str,
@@ -80,6 +94,8 @@ def __init__(
8094
agent_details: "AgentDetails | None" = None,
8195
tenant_details: "TenantDetails | None" = None,
8296
parent_id: str | None = None,
97+
start_time: datetime | None = None,
98+
end_time: datetime | None = None,
8399
):
84100
"""Initialize the OpenTelemetry scope.
85101
@@ -91,9 +107,15 @@ def __init__(
91107
tenant_details: Optional tenant details
92108
parent_id: Optional parent Activity ID used to link this span to an upstream
93109
operation
110+
start_time: Optional explicit start time as a datetime object.
111+
Useful when recording an operation after it has already completed.
112+
end_time: Optional explicit end time as a datetime object.
113+
When provided, the span will use this timestamp when disposed
114+
instead of the current wall-clock time.
94115
"""
95116
self._span: Span | None = None
96-
self._start_time = time.time()
117+
self._custom_start_time: datetime | None = start_time
118+
self._custom_end_time: datetime | None = end_time
97119
self._has_ended = False
98120
self._error_type: str | None = None
99121
self._exception: Exception | None = None
@@ -119,7 +141,15 @@ def __init__(
119141
parent_context = parse_parent_id_to_context(parent_id)
120142
span_context = parent_context if parent_context else context.get_current()
121143

122-
self._span = tracer.start_span(activity_name, kind=activity_kind, context=span_context)
144+
# Convert custom start time to OTel-compatible format (nanoseconds since epoch)
145+
otel_start_time = self._datetime_to_ns(start_time)
146+
147+
self._span = tracer.start_span(
148+
activity_name,
149+
kind=activity_kind,
150+
context=span_context,
151+
start_time=otel_start_time,
152+
)
123153

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

263+
def set_end_time(self, end_time: datetime) -> None:
264+
"""Set a custom end time for the scope.
265+
266+
When set, dispose() will pass this value to span.end() instead of using
267+
the current wall-clock time. This is useful when the actual end time of
268+
the operation is known before the scope is disposed.
269+
270+
Args:
271+
end_time: The end time as a datetime object.
272+
"""
273+
self._custom_end_time = end_time
274+
233275
def _end(self) -> None:
234276
"""End the span and record metrics."""
235277
if self._span and self._is_telemetry_enabled() and not self._has_ended:
236278
self._has_ended = True
237279
span_id = f"{self._span.context.span_id:016x}" if self._span.context else "unknown"
238280
logger.info(f"Span ended: '{self._span.name}' ({span_id})")
239281

240-
self._span.end()
282+
# Convert custom end time to OTel-compatible format (nanoseconds since epoch)
283+
otel_end_time = self._datetime_to_ns(self._custom_end_time)
284+
if otel_end_time is not None:
285+
self._span.end(end_time=otel_end_time)
286+
else:
287+
self._span.end()
241288

242289
def __enter__(self):
243290
"""Enter the context manager and make span active."""

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/spans_scopes/output_scope.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4+
from datetime import datetime
5+
46
from ..agent_details import AgentDetails
57
from ..constants import GEN_AI_OUTPUT_MESSAGES_KEY
68
from ..models.response import Response
@@ -20,6 +22,8 @@ def start(
2022
tenant_details: TenantDetails,
2123
response: Response,
2224
parent_id: str | None = None,
25+
start_time: datetime | None = None,
26+
end_time: datetime | None = None,
2327
) -> "OutputScope":
2428
"""Creates and starts a new scope for output tracing.
2529
@@ -29,18 +33,22 @@ def start(
2933
response: The response details from the agent
3034
parent_id: Optional parent Activity ID used to link this span to an upstream
3135
operation
36+
start_time: Optional explicit start time as a datetime object.
37+
end_time: Optional explicit end time as a datetime object.
3238
3339
Returns:
3440
A new OutputScope instance
3541
"""
36-
return OutputScope(agent_details, tenant_details, response, parent_id)
42+
return OutputScope(agent_details, tenant_details, response, parent_id, start_time, end_time)
3743

3844
def __init__(
3945
self,
4046
agent_details: AgentDetails,
4147
tenant_details: TenantDetails,
4248
response: Response,
4349
parent_id: str | None = None,
50+
start_time: datetime | None = None,
51+
end_time: datetime | None = None,
4452
):
4553
"""Initialize the output scope.
4654
@@ -50,6 +58,8 @@ def __init__(
5058
response: The response details from the agent
5159
parent_id: Optional parent Activity ID used to link this span to an upstream
5260
operation
61+
start_time: Optional explicit start time as a datetime object.
62+
end_time: Optional explicit end time as a datetime object.
5363
"""
5464
super().__init__(
5565
kind="Client",
@@ -58,6 +68,8 @@ def __init__(
5868
agent_details=agent_details,
5969
tenant_details=tenant_details,
6070
parent_id=parent_id,
71+
start_time=start_time,
72+
end_time=end_time,
6173
)
6274

6375
# Initialize accumulated messages list

0 commit comments

Comments
 (0)