Skip to content

Commit 8354142

Browse files
CopilotnikhilNava
andcommitted
Simplify TimeInput type to just use datetime
Based on user feedback, simplify the start_time/end_time parameters to only accept datetime objects instead of the complex TimeInput union type. This is more Pythonic and consistent with existing code in the LangChain tracer. Changes: - Remove TimeInput type alias - Change start_time/end_time parameters to datetime | None - Add simple _datetime_to_ns() conversion method - Remove unused _time_input_to_ns() and _time_input_to_ms() methods - Update all scope subclasses to use datetime | None - Simplify tests to only use datetime Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com>
1 parent a2f3385 commit 8354142

File tree

7 files changed

+82
-209
lines changed

7 files changed

+82
-209
lines changed

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from .invoke_agent_details import InvokeAgentDetails
2525
from .invoke_agent_scope import InvokeAgentScope
2626
from .middleware.baggage_builder import BaggageBuilder
27-
from .opentelemetry_scope import OpenTelemetryScope, TimeInput
27+
from .opentelemetry_scope import OpenTelemetryScope
2828
from .request import Request
2929
from .source_metadata import SourceMetadata
3030
from .tenant_details import TenantDetails
@@ -47,8 +47,6 @@
4747
"SpanProcessor",
4848
# Base scope class
4949
"OpenTelemetryScope",
50-
# Type aliases
51-
"TimeInput",
5250
# Specific scope classes
5351
"ExecuteToolScope",
5452
"InvokeAgentScope",

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

Lines changed: 13 additions & 13 deletions
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,
@@ -15,7 +17,7 @@
1517
SERVER_ADDRESS_KEY,
1618
SERVER_PORT_KEY,
1719
)
18-
from .opentelemetry_scope import OpenTelemetryScope, TimeInput
20+
from .opentelemetry_scope import OpenTelemetryScope
1921
from .request import Request
2022
from .tenant_details import TenantDetails
2123
from .tool_call_details import ToolCallDetails
@@ -31,8 +33,8 @@ def start(
3133
tenant_details: TenantDetails,
3234
request: Request | None = None,
3335
parent_id: str | None = None,
34-
start_time: TimeInput = None,
35-
end_time: TimeInput = None,
36+
start_time: datetime | None = None,
37+
end_time: datetime | None = None,
3638
) -> "ExecuteToolScope":
3739
"""Creates and starts a new scope for tool execution tracing.
3840
@@ -43,11 +45,10 @@ def start(
4345
request: Optional request details for additional context
4446
parent_id: Optional parent Activity ID used to link this span to an upstream
4547
operation
46-
start_time: Optional explicit start time. Accepts int (nanoseconds since epoch),
47-
float (seconds since epoch), tuple[int, int] (HrTime), or datetime. Useful when
48+
start_time: Optional explicit start time as a datetime object. Useful when
4849
recording a tool call after execution has already completed.
49-
end_time: Optional explicit end time in the same formats as start_time. When
50-
provided, the span will use this timestamp when disposed instead of the
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
5152
current wall-clock time.
5253
5354
Returns:
@@ -64,8 +65,8 @@ def __init__(
6465
tenant_details: TenantDetails,
6566
request: Request | None = None,
6667
parent_id: str | None = None,
67-
start_time: TimeInput = None,
68-
end_time: TimeInput = None,
68+
start_time: datetime | None = None,
69+
end_time: datetime | None = None,
6970
):
7071
"""Initialize the tool execution scope.
7172
@@ -76,11 +77,10 @@ def __init__(
7677
request: Optional request details for additional context
7778
parent_id: Optional parent Activity ID used to link this span to an upstream
7879
operation
79-
start_time: Optional explicit start time. Accepts int (nanoseconds since epoch),
80-
float (seconds since epoch), tuple[int, int] (HrTime), or datetime. Useful when
80+
start_time: Optional explicit start time as a datetime object. Useful when
8181
recording a tool call after execution has already completed.
82-
end_time: Optional explicit end time in the same formats as start_time. When
83-
provided, the span will use this timestamp when disposed instead of the
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
8484
current wall-clock time.
8585
"""
8686
super().__init__(

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

Lines changed: 10 additions & 11 deletions
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
@@ -19,7 +20,7 @@
1920
GEN_AI_USAGE_OUTPUT_TOKENS_KEY,
2021
)
2122
from .inference_call_details import InferenceCallDetails
22-
from .opentelemetry_scope import OpenTelemetryScope, TimeInput
23+
from .opentelemetry_scope import OpenTelemetryScope
2324
from .request import Request
2425
from .tenant_details import TenantDetails
2526
from .utils import safe_json_dumps
@@ -35,8 +36,8 @@ def start(
3536
tenant_details: TenantDetails,
3637
request: Request | None = None,
3738
parent_id: str | None = None,
38-
start_time: TimeInput = None,
39-
end_time: TimeInput = None,
39+
start_time: datetime | None = None,
40+
end_time: datetime | None = None,
4041
) -> "InferenceScope":
4142
"""Creates and starts a new scope for inference tracing.
4243
@@ -47,9 +48,8 @@ def start(
4748
request: Optional request details for additional context
4849
parent_id: Optional parent Activity ID used to link this span to an upstream
4950
operation
50-
start_time: Optional explicit start time. Accepts int (nanoseconds since epoch),
51-
float (seconds since epoch), tuple[int, int] (HrTime), or datetime.
52-
end_time: Optional explicit end time in the same formats as start_time.
51+
start_time: Optional explicit start time as a datetime object.
52+
end_time: Optional explicit end time as a datetime object.
5353
5454
Returns:
5555
A new InferenceScope instance
@@ -65,8 +65,8 @@ def __init__(
6565
tenant_details: TenantDetails,
6666
request: Request | None = None,
6767
parent_id: str | None = None,
68-
start_time: TimeInput = None,
69-
end_time: TimeInput = None,
68+
start_time: datetime | None = None,
69+
end_time: datetime | None = None,
7070
):
7171
"""Initialize the inference scope.
7272
@@ -77,9 +77,8 @@ def __init__(
7777
request: Optional request details for additional context
7878
parent_id: Optional parent Activity ID used to link this span to an upstream
7979
operation
80-
start_time: Optional explicit start time. Accepts int (nanoseconds since epoch),
81-
float (seconds since epoch), tuple[int, int] (HrTime), or datetime.
82-
end_time: Optional explicit end time in the same formats as start_time.
80+
start_time: Optional explicit start time as a datetime object.
81+
end_time: Optional explicit end time as a datetime object.
8382
"""
8483

8584
super().__init__(

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

Lines changed: 10 additions & 11 deletions
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 (
@@ -32,7 +33,7 @@
3233
)
3334
from .invoke_agent_details import InvokeAgentDetails
3435
from .models.caller_details import CallerDetails
35-
from .opentelemetry_scope import OpenTelemetryScope, TimeInput
36+
from .opentelemetry_scope import OpenTelemetryScope
3637
from .request import Request
3738
from .tenant_details import TenantDetails
3839
from .utils import safe_json_dumps, validate_and_normalize_ip
@@ -50,8 +51,8 @@ def start(
5051
request: Request | None = None,
5152
caller_agent_details: AgentDetails | None = None,
5253
caller_details: CallerDetails | None = None,
53-
start_time: TimeInput = None,
54-
end_time: TimeInput = None,
54+
start_time: datetime | None = None,
55+
end_time: datetime | None = None,
5556
) -> "InvokeAgentScope":
5657
"""Create and start a new scope for agent invocation tracing.
5758
@@ -62,9 +63,8 @@ def start(
6263
request: Optional request details for additional context
6364
caller_agent_details: Optional details of the caller agent
6465
caller_details: Optional details of the non-agentic caller
65-
start_time: Optional explicit start time. Accepts int (nanoseconds since epoch),
66-
float (seconds since epoch), tuple[int, int] (HrTime), or datetime.
67-
end_time: Optional explicit end time in the same formats as start_time.
66+
start_time: Optional explicit start time as a datetime object.
67+
end_time: Optional explicit end time as a datetime object.
6868
6969
Returns:
7070
A new InvokeAgentScope instance
@@ -86,8 +86,8 @@ def __init__(
8686
request: Request | None = None,
8787
caller_agent_details: AgentDetails | None = None,
8888
caller_details: CallerDetails | None = None,
89-
start_time: TimeInput = None,
90-
end_time: TimeInput = None,
89+
start_time: datetime | None = None,
90+
end_time: datetime | None = None,
9191
):
9292
"""Initialize the agent invocation scope.
9393
@@ -97,9 +97,8 @@ def __init__(
9797
request: Optional request details for additional context
9898
caller_agent_details: Optional details of the caller agent
9999
caller_details: Optional details of the non-agentic caller
100-
start_time: Optional explicit start time. Accepts int (nanoseconds since epoch),
101-
float (seconds since epoch), tuple[int, int] (HrTime), or datetime.
102-
end_time: Optional explicit end time in the same formats as start_time.
100+
start_time: Optional explicit start time as a datetime object.
101+
end_time: Optional explicit end time as a datetime object.
103102
"""
104103
activity_name = INVOKE_AGENT_OPERATION_NAME
105104
if invoke_agent_details.details.agent_name:

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

Lines changed: 24 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,6 @@
4848
# Create logger for this module - inherits from 'microsoft_agents_a365.observability.core'
4949
logger = logging.getLogger(__name__)
5050

51-
# TimeInput is a type alias for types that can be used as span timestamps.
52-
# OpenTelemetry Python SDK accepts int (nanoseconds since epoch), float (seconds since epoch),
53-
# or a tuple of (seconds, nanoseconds) as HrTime.
54-
# We extend this to also accept datetime objects for convenience.
55-
TimeInput = int | float | tuple[int, int] | datetime | None
56-
5751

5852
class OpenTelemetryScope:
5953
"""Base class for OpenTelemetry tracing scopes in the SDK."""
@@ -78,6 +72,20 @@ def _is_telemetry_enabled(cls) -> bool:
7872
enable_observability = os.getenv(ENABLE_A365_OBSERVABILITY, "").lower()
7973
return (env_value or enable_observability) in ("true", "1", "yes", "on")
8074

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+
8189
def __init__(
8290
self,
8391
kind: str,
@@ -86,8 +94,8 @@ def __init__(
8694
agent_details: "AgentDetails | None" = None,
8795
tenant_details: "TenantDetails | None" = None,
8896
parent_id: str | None = None,
89-
start_time: TimeInput = None,
90-
end_time: TimeInput = None,
97+
start_time: datetime | None = None,
98+
end_time: datetime | None = None,
9199
):
92100
"""Initialize the OpenTelemetry scope.
93101
@@ -99,19 +107,15 @@ def __init__(
99107
tenant_details: Optional tenant details
100108
parent_id: Optional parent Activity ID used to link this span to an upstream
101109
operation
102-
start_time: Optional explicit start time. Can be:
103-
- int: nanoseconds since epoch
104-
- float: seconds since epoch
105-
- tuple[int, int]: HrTime as (seconds, nanoseconds)
106-
- datetime: Python datetime object
110+
start_time: Optional explicit start time as a datetime object.
107111
Useful when recording an operation after it has already completed.
108-
end_time: Optional explicit end time in the same format as start_time.
112+
end_time: Optional explicit end time as a datetime object.
109113
When provided, the span will use this timestamp when disposed
110114
instead of the current wall-clock time.
111115
"""
112116
self._span: Span | None = None
113-
self._custom_start_time: TimeInput = start_time
114-
self._custom_end_time: TimeInput = end_time
117+
self._custom_start_time: datetime | None = start_time
118+
self._custom_end_time: datetime | None = end_time
115119
self._has_ended = False
116120
self._error_type: str | None = None
117121
self._exception: Exception | None = None
@@ -138,7 +142,7 @@ def __init__(
138142
span_context = parent_context if parent_context else context.get_current()
139143

140144
# Convert custom start time to OTel-compatible format (nanoseconds since epoch)
141-
otel_start_time = self._time_input_to_ns(start_time) if start_time else None
145+
otel_start_time = self._datetime_to_ns(start_time)
142146

143147
self._span = tracer.start_span(
144148
activity_name,
@@ -256,68 +260,15 @@ def record_attributes(self, attributes: dict[str, Any] | list[tuple[str, Any]])
256260
if key and key.strip():
257261
self._span.set_attribute(key, value)
258262

259-
@staticmethod
260-
def _time_input_to_ns(t: TimeInput) -> int | None:
261-
"""Convert a TimeInput value to nanoseconds since epoch.
262-
263-
OpenTelemetry Python SDK accepts int (nanoseconds since epoch) for span
264-
start_time and end_time parameters.
265-
266-
Args:
267-
t: TimeInput value which can be:
268-
- int: nanoseconds since epoch (returned as-is)
269-
- float: seconds since epoch
270-
- tuple[int, int]: HrTime as (seconds, nanoseconds)
271-
- datetime: Python datetime object
272-
273-
Returns:
274-
Nanoseconds since epoch, or None if input is None
275-
"""
276-
if t is None:
277-
return None
278-
if isinstance(t, int):
279-
# Assume nanoseconds if it's an integer
280-
return t
281-
if isinstance(t, float):
282-
# Convert seconds to nanoseconds
283-
return int(t * 1_000_000_000)
284-
if isinstance(t, tuple) and len(t) == 2:
285-
# HrTime: (seconds, nanoseconds)
286-
seconds, nanos = t
287-
return seconds * 1_000_000_000 + nanos
288-
if isinstance(t, datetime):
289-
return int(t.timestamp() * 1_000_000_000)
290-
logger.warning(
291-
f"_time_input_to_ns received unexpected TimeInput "
292-
f"(type={type(t).__name__}); returning None"
293-
)
294-
return None
295-
296-
@staticmethod
297-
def _time_input_to_ms(t: TimeInput) -> float | None:
298-
"""Convert a TimeInput value to milliseconds since epoch.
299-
300-
Used for duration calculations.
301-
302-
Args:
303-
t: TimeInput value (see _time_input_to_ns for accepted types)
304-
305-
Returns:
306-
Milliseconds since epoch, or None if input is None
307-
"""
308-
ns = OpenTelemetryScope._time_input_to_ns(t)
309-
return ns / 1_000_000 if ns is not None else None
310-
311-
def set_end_time(self, end_time: TimeInput) -> None:
263+
def set_end_time(self, end_time: datetime) -> None:
312264
"""Set a custom end time for the scope.
313265
314266
When set, dispose() will pass this value to span.end() instead of using
315267
the current wall-clock time. This is useful when the actual end time of
316268
the operation is known before the scope is disposed.
317269
318270
Args:
319-
end_time: The end time as nanoseconds since epoch, seconds since epoch,
320-
HrTime tuple (seconds, nanoseconds), or datetime object.
271+
end_time: The end time as a datetime object.
321272
"""
322273
self._custom_end_time = end_time
323274

@@ -329,7 +280,7 @@ def _end(self) -> None:
329280
logger.info(f"Span ended: '{self._span.name}' ({span_id})")
330281

331282
# Convert custom end time to OTel-compatible format (nanoseconds since epoch)
332-
otel_end_time = self._time_input_to_ns(self._custom_end_time)
283+
otel_end_time = self._datetime_to_ns(self._custom_end_time)
333284
if otel_end_time is not None:
334285
self._span.end(end_time=otel_end_time)
335286
else:

0 commit comments

Comments
 (0)