Skip to content

Commit ba8566d

Browse files
CopilotnikhilNavaCopilotnikhilc-microsoft
authored
Replace parent_id string with OpenTelemetry Context for trace propagation (#205)
* Initial plan * feat: Replace parent_id string with Context object and add context propagation - Changed all scope classes to accept `parent_context: Context` instead of `parent_id: str` - Added `inject_trace_context()` method to OpenTelemetryScope for header propagation - Added `get_context()` method to get the scope's Context for passing to child scopes - Added `extract_trace_context()` utility function using OTel's standard `extract()` - Removed custom W3C trace context parsing in favor of OTel's propagation API - Updated OutputLoggingMiddleware to use extract_trace_context() - Updated all tests to use extract_trace_context() and parent_context Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com> * refactor: Address code review feedback - move imports to module level Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * clean up usage of preant context propogation --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com> Co-authored-by: Nikhil Navakiran <nikhil.navakiran@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Nikhil Chitlur Navakiran (from Dev Box) <nikhilc@microsoft.com>
1 parent 0cb4f2a commit ba8566d

15 files changed

Lines changed: 471 additions & 161 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from .tool_call_details import ToolCallDetails
3434
from .tool_type import ToolType
3535
from .trace_processor.span_processor import SpanProcessor
36+
from .utils import extract_context_from_headers, get_traceparent
3637

3738
__all__ = [
3839
# Main SDK functions
@@ -71,6 +72,9 @@
7172
"ExecutionType",
7273
"InferenceOperationType",
7374
"ToolType",
75+
# Utility functions
76+
"extract_context_from_headers",
77+
"get_traceparent",
7478
# Constants
7579
# all constants from constants.py are exported via *
7680
]

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from datetime import datetime
55

6+
from opentelemetry.context import Context
67
from opentelemetry.trace import SpanKind
78

89
from .agent_details import AgentDetails
@@ -33,7 +34,7 @@ def start(
3334
agent_details: AgentDetails,
3435
tenant_details: TenantDetails,
3536
request: Request | None = None,
36-
parent_id: str | None = None,
37+
parent_context: Context | None = None,
3738
start_time: datetime | None = None,
3839
end_time: datetime | None = None,
3940
span_kind: SpanKind | None = None,
@@ -45,8 +46,9 @@ def start(
4546
agent_details: The details of the agent making the call
4647
tenant_details: The details of the tenant
4748
request: Optional request details for additional context
48-
parent_id: Optional parent Activity ID used to link this span to an upstream
49-
operation
49+
parent_context: Optional OpenTelemetry Context used to link this span to an
50+
upstream operation. Use ``extract_context_from_headers()`` to convert a
51+
Context from HTTP headers containing W3C traceparent.
5052
start_time: Optional explicit start time as a datetime object. Useful when
5153
recording a tool call after execution has already completed.
5254
end_time: Optional explicit end time as a datetime object. When provided,
@@ -63,7 +65,7 @@ def start(
6365
agent_details,
6466
tenant_details,
6567
request,
66-
parent_id,
68+
parent_context,
6769
start_time,
6870
end_time,
6971
span_kind,
@@ -75,7 +77,7 @@ def __init__(
7577
agent_details: AgentDetails,
7678
tenant_details: TenantDetails,
7779
request: Request | None = None,
78-
parent_id: str | None = None,
80+
parent_context: Context | None = None,
7981
start_time: datetime | None = None,
8082
end_time: datetime | None = None,
8183
span_kind: SpanKind | None = None,
@@ -87,8 +89,9 @@ def __init__(
8789
agent_details: The details of the agent making the call
8890
tenant_details: The details of the tenant
8991
request: Optional request details for additional context
90-
parent_id: Optional parent Activity ID used to link this span to an upstream
91-
operation
92+
parent_context: Optional OpenTelemetry Context used to link this span to an
93+
upstream operation. Use ``extract_context_from_headers()`` to convert a
94+
Context from HTTP headers containing W3C traceparent.
9295
start_time: Optional explicit start time as a datetime object. Useful when
9396
recording a tool call after execution has already completed.
9497
end_time: Optional explicit end time as a datetime object. When provided,
@@ -103,7 +106,7 @@ def __init__(
103106
activity_name=f"{EXECUTE_TOOL_OPERATION_NAME} {details.tool_name}",
104107
agent_details=agent_details,
105108
tenant_details=tenant_details,
106-
parent_id=parent_id,
109+
parent_context=parent_context,
107110
start_time=start_time,
108111
end_time=end_time,
109112
)

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from datetime import datetime
55
from typing import List
66

7+
from opentelemetry.context import Context
8+
79
from .agent_details import AgentDetails
810
from .constants import (
911
CHANNEL_LINK_KEY,
@@ -36,7 +38,7 @@ def start(
3638
agent_details: AgentDetails,
3739
tenant_details: TenantDetails,
3840
request: Request | None = None,
39-
parent_id: str | None = None,
41+
parent_context: Context | None = None,
4042
start_time: datetime | None = None,
4143
end_time: datetime | None = None,
4244
) -> "InferenceScope":
@@ -47,16 +49,17 @@ def start(
4749
agent_details: The details of the agent making the call
4850
tenant_details: The details of the tenant
4951
request: Optional request details for additional context
50-
parent_id: Optional parent Activity ID used to link this span to an upstream
51-
operation
52+
parent_context: Optional OpenTelemetry Context used to link this span to an
53+
upstream operation. Use ``extract_context_from_headers()`` to convert a
54+
Context from HTTP headers containing W3C traceparent.
5255
start_time: Optional explicit start time as a datetime object.
5356
end_time: Optional explicit end time as a datetime object.
5457
5558
Returns:
5659
A new InferenceScope instance
5760
"""
5861
return InferenceScope(
59-
details, agent_details, tenant_details, request, parent_id, start_time, end_time
62+
details, agent_details, tenant_details, request, parent_context, start_time, end_time
6063
)
6164

6265
def __init__(
@@ -65,7 +68,7 @@ def __init__(
6568
agent_details: AgentDetails,
6669
tenant_details: TenantDetails,
6770
request: Request | None = None,
68-
parent_id: str | None = None,
71+
parent_context: Context | None = None,
6972
start_time: datetime | None = None,
7073
end_time: datetime | None = None,
7174
):
@@ -76,8 +79,9 @@ def __init__(
7679
agent_details: The details of the agent making the call
7780
tenant_details: The details of the tenant
7881
request: Optional request details for additional context
79-
parent_id: Optional parent Activity ID used to link this span to an upstream
80-
operation
82+
parent_context: Optional OpenTelemetry Context used to link this span to an
83+
upstream operation. Use ``extract_context_from_headers()`` to convert a
84+
Context from HTTP headers containing W3C traceparent.
8185
start_time: Optional explicit start time as a datetime object.
8286
end_time: Optional explicit end time as a datetime object.
8387
"""
@@ -88,7 +92,7 @@ def __init__(
8892
activity_name=f"{details.operationName.value} {details.model}",
8993
agent_details=agent_details,
9094
tenant_details=tenant_details,
91-
parent_id=parent_id,
95+
parent_context=parent_context,
9296
start_time=start_time,
9397
end_time=end_time,
9498
)

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
from datetime import datetime
88

9+
from opentelemetry.context import Context
910
from opentelemetry.trace import SpanKind
1011

1112
from .agent_details import AgentDetails
@@ -50,6 +51,7 @@ def start(
5051
request: Request | None = None,
5152
caller_agent_details: AgentDetails | None = None,
5253
caller_details: CallerDetails | None = None,
54+
parent_context: Context | None = None,
5355
start_time: datetime | None = None,
5456
end_time: datetime | None = None,
5557
span_kind: SpanKind | None = None,
@@ -63,6 +65,9 @@ def start(
6365
request: Optional request details for additional context
6466
caller_agent_details: Optional details of the caller agent
6567
caller_details: Optional details of the non-agentic caller
68+
parent_context: Optional OpenTelemetry Context used to link this span to an
69+
upstream operation. Use ``extract_context_from_headers()`` to convert a
70+
Context from HTTP headers containing W3C traceparent.
6671
start_time: Optional explicit start time as a datetime object.
6772
end_time: Optional explicit end time as a datetime object.
6873
span_kind: Optional span kind override. Defaults to ``SpanKind.CLIENT``.
@@ -77,6 +82,7 @@ def start(
7782
request,
7883
caller_agent_details,
7984
caller_details,
85+
parent_context,
8086
start_time,
8187
end_time,
8288
span_kind,
@@ -89,6 +95,7 @@ def __init__(
8995
request: Request | None = None,
9096
caller_agent_details: AgentDetails | None = None,
9197
caller_details: CallerDetails | None = None,
98+
parent_context: Context | None = None,
9299
start_time: datetime | None = None,
93100
end_time: datetime | None = None,
94101
span_kind: SpanKind | None = None,
@@ -101,6 +108,9 @@ def __init__(
101108
request: Optional request details for additional context
102109
caller_agent_details: Optional details of the caller agent
103110
caller_details: Optional details of the non-agentic caller
111+
parent_context: Optional OpenTelemetry Context used to link this span to an
112+
upstream operation. Use ``extract_context_from_headers()`` to convert a
113+
Context from HTTP headers containing W3C traceparent.
104114
start_time: Optional explicit start time as a datetime object.
105115
end_time: Optional explicit end time as a datetime object.
106116
span_kind: Optional span kind override. Defaults to ``SpanKind.CLIENT``.
@@ -118,6 +128,7 @@ def __init__(
118128
activity_name=activity_name,
119129
agent_details=invoke_agent_details.details,
120130
tenant_details=tenant_details,
131+
parent_context=parent_context,
121132
start_time=start_time,
122133
end_time=end_time,
123134
)

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

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from typing import TYPE_CHECKING, Any
1111

1212
from opentelemetry import context, trace
13+
from opentelemetry.context import Context
14+
from opentelemetry.propagate import inject
1315
from opentelemetry.trace import (
1416
Span,
1517
SpanKind,
@@ -43,7 +45,7 @@
4345
TELEMETRY_SDK_VERSION_KEY,
4446
TENANT_ID_KEY,
4547
)
46-
from .utils import get_sdk_version, parse_parent_id_to_context
48+
from .utils import get_sdk_version
4749

4850
if TYPE_CHECKING:
4951
from .agent_details import AgentDetails
@@ -97,7 +99,7 @@ def __init__(
9799
activity_name: str,
98100
agent_details: "AgentDetails | None" = None,
99101
tenant_details: "TenantDetails | None" = None,
100-
parent_id: str | None = None,
102+
parent_context: Context | None = None,
101103
start_time: datetime | None = None,
102104
end_time: datetime | None = None,
103105
):
@@ -111,8 +113,9 @@ def __init__(
111113
activity_name: The name of the activity for display purposes
112114
agent_details: Optional agent details
113115
tenant_details: Optional tenant details
114-
parent_id: Optional parent Activity ID used to link this span to an upstream
115-
operation
116+
parent_context: Optional OpenTelemetry Context used to link this span to an
117+
upstream operation. Use ``extract_context_from_headers()`` to extract a
118+
Context from HTTP headers containing W3C traceparent.
116119
start_time: Optional explicit start time as a datetime object.
117120
Useful when recording an operation after it has already completed.
118121
end_time: Optional explicit end time as a datetime object.
@@ -146,9 +149,8 @@ def __init__(
146149
activity_kind = SpanKind.CONSUMER
147150

148151
# Get context for parent relationship
149-
# If parent_id is provided, parse it and use it as the parent context
152+
# If parent_context is provided, use it directly
150153
# Otherwise, use the current context
151-
parent_context = parse_parent_id_to_context(parent_id)
152154
span_context = parent_context if parent_context else context.get_current()
153155

154156
# Convert custom start time to OTel-compatible format (nanoseconds since epoch)
@@ -286,6 +288,49 @@ def _end(self) -> None:
286288
else:
287289
self._span.end()
288290

291+
def get_context(self) -> Context | None:
292+
"""Get the OpenTelemetry context for this scope's span.
293+
294+
This method returns a Context object containing this scope's span,
295+
which can be used to propagate trace context to child operations
296+
or downstream services.
297+
298+
Returns:
299+
A Context containing this scope's span, or None if telemetry
300+
is disabled or no span exists.
301+
"""
302+
if self._span and self._is_telemetry_enabled():
303+
return set_span_in_context(self._span)
304+
return None
305+
306+
def inject_context_to_headers(self) -> dict[str, str]:
307+
"""Inject this span's trace context into W3C HTTP headers.
308+
309+
Returns a dictionary of headers containing ``traceparent`` and
310+
optionally ``tracestate`` that can be forwarded to downstream services
311+
or stored for later context propagation.
312+
313+
Example usage:
314+
315+
.. code-block:: python
316+
317+
scope = OpenTelemetryScope(...)
318+
headers = scope.inject_context_to_headers()
319+
# Add headers to outgoing HTTP request
320+
requests.get("https://downstream-service/api", headers=headers)
321+
322+
Returns:
323+
A dictionary containing W3C trace context headers. Returns an
324+
empty dictionary if telemetry is disabled or no span exists.
325+
"""
326+
headers: dict[str, str] = {}
327+
if self._span and self._is_telemetry_enabled():
328+
# Create a context with the current span
329+
ctx = set_span_in_context(self._span)
330+
# Use the global propagator to inject trace context into headers
331+
inject(headers, context=ctx)
332+
return headers
333+
289334
def __enter__(self):
290335
"""Enter the context manager and make span active."""
291336
if self._span and self._is_telemetry_enabled():

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

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from datetime import datetime
55

6+
from opentelemetry.context import Context
7+
68
from ..agent_details import AgentDetails
79
from ..constants import GEN_AI_OUTPUT_MESSAGES_KEY
810
from ..models.response import Response
@@ -23,7 +25,7 @@ def start(
2325
agent_details: AgentDetails,
2426
tenant_details: TenantDetails,
2527
response: Response,
26-
parent_id: str | None = None,
28+
parent_context: Context | None = None,
2729
start_time: datetime | None = None,
2830
end_time: datetime | None = None,
2931
) -> "OutputScope":
@@ -33,22 +35,25 @@ def start(
3335
agent_details: The details of the agent
3436
tenant_details: The details of the tenant
3537
response: The response details from the agent
36-
parent_id: Optional parent Activity ID used to link this span to an upstream
37-
operation
38+
parent_context: Optional OpenTelemetry Context used to link this span to an
39+
upstream operation. Use ``extract_context_from_headers()`` to convert a
40+
Context from HTTP headers containing W3C traceparent.
3841
start_time: Optional explicit start time as a datetime object.
3942
end_time: Optional explicit end time as a datetime object.
4043
4144
Returns:
4245
A new OutputScope instance
4346
"""
44-
return OutputScope(agent_details, tenant_details, response, parent_id, start_time, end_time)
47+
return OutputScope(
48+
agent_details, tenant_details, response, parent_context, start_time, end_time
49+
)
4550

4651
def __init__(
4752
self,
4853
agent_details: AgentDetails,
4954
tenant_details: TenantDetails,
5055
response: Response,
51-
parent_id: str | None = None,
56+
parent_context: Context | None = None,
5257
start_time: datetime | None = None,
5358
end_time: datetime | None = None,
5459
):
@@ -58,8 +63,9 @@ def __init__(
5863
agent_details: The details of the agent
5964
tenant_details: The details of the tenant
6065
response: The response details from the agent
61-
parent_id: Optional parent Activity ID used to link this span to an upstream
62-
operation
66+
parent_context: Optional OpenTelemetry Context used to link this span to an
67+
upstream operation. Use ``extract_context_from_headers()`` to convert a
68+
Context from HTTP headers containing W3C traceparent.
6369
start_time: Optional explicit start time as a datetime object.
6470
end_time: Optional explicit end time as a datetime object.
6571
"""
@@ -69,7 +75,7 @@ def __init__(
6975
activity_name=(f"{OUTPUT_OPERATION_NAME} {agent_details.agent_id}"),
7076
agent_details=agent_details,
7177
tenant_details=tenant_details,
72-
parent_id=parent_id,
78+
parent_context=parent_context,
7379
start_time=start_time,
7480
end_time=end_time,
7581
)

0 commit comments

Comments
 (0)