microsoft-agents-a365-observability-core 0.2.1.dev36__tar.gz → 0.2.1.dev43__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/PKG-INFO +1 -1
  2. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/__init__.py +4 -0
  3. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/execute_tool_scope.py +11 -8
  4. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py +26 -13
  5. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/exporters/utils.py +34 -0
  6. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/inference_scope.py +12 -8
  7. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/invoke_agent_scope.py +11 -0
  8. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/opentelemetry_scope.py +52 -22
  9. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/spans_scopes/output_scope.py +19 -8
  10. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/utils.py +35 -102
  11. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365_observability_core.egg-info/PKG-INFO +1 -1
  12. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/README.md +0 -0
  13. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/agent_details.py +0 -0
  14. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/config.py +0 -0
  15. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/constants.py +0 -0
  16. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/execution_type.py +0 -0
  17. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/exporters/__init__.py +0 -0
  18. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/exporters/agent365_exporter_options.py +0 -0
  19. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/exporters/enriched_span.py +0 -0
  20. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/exporters/enriching_span_processor.py +0 -0
  21. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/exporters/spectra_exporter_options.py +0 -0
  22. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/inference_call_details.py +0 -0
  23. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/inference_operation_type.py +0 -0
  24. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/invoke_agent_details.py +0 -0
  25. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/middleware/__init__.py +0 -0
  26. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/middleware/baggage_builder.py +0 -0
  27. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/models/__init__.py +0 -0
  28. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/models/agent_type.py +0 -0
  29. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/models/caller_details.py +0 -0
  30. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/models/operation_source.py +0 -0
  31. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/models/response.py +0 -0
  32. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/request.py +0 -0
  33. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/source_metadata.py +0 -0
  34. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/spans_scopes/__init__.py +0 -0
  35. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/tenant_details.py +0 -0
  36. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/tool_call_details.py +0 -0
  37. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/tool_type.py +0 -0
  38. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/trace_processor/__init__.py +0 -0
  39. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/trace_processor/span_processor.py +0 -0
  40. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365/observability/core/trace_processor/util.py +0 -0
  41. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365_observability_core.egg-info/SOURCES.txt +0 -0
  42. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365_observability_core.egg-info/dependency_links.txt +0 -0
  43. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365_observability_core.egg-info/requires.txt +0 -0
  44. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/microsoft_agents_a365_observability_core.egg-info/top_level.txt +0 -0
  45. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/pyproject.toml +0 -0
  46. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/setup.cfg +0 -0
  47. {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev43}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-observability-core
3
- Version: 0.2.1.dev36
3
+ Version: 0.2.1.dev43
4
4
  Summary: Telemetry, tracing, and monitoring components for AI agents
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -33,6 +33,7 @@ from .tenant_details import TenantDetails
33
33
  from .tool_call_details import ToolCallDetails
34
34
  from .tool_type import ToolType
35
35
  from .trace_processor.span_processor import SpanProcessor
36
+ from .utils import extract_context_from_headers, get_traceparent
36
37
 
37
38
  __all__ = [
38
39
  # Main SDK functions
@@ -71,6 +72,9 @@ __all__ = [
71
72
  "ExecutionType",
72
73
  "InferenceOperationType",
73
74
  "ToolType",
75
+ # Utility functions
76
+ "extract_context_from_headers",
77
+ "get_traceparent",
74
78
  # Constants
75
79
  # all constants from constants.py are exported via *
76
80
  ]
@@ -3,6 +3,7 @@
3
3
 
4
4
  from datetime import datetime
5
5
 
6
+ from opentelemetry.context import Context
6
7
  from opentelemetry.trace import SpanKind
7
8
 
8
9
  from .agent_details import AgentDetails
@@ -33,7 +34,7 @@ class ExecuteToolScope(OpenTelemetryScope):
33
34
  agent_details: AgentDetails,
34
35
  tenant_details: TenantDetails,
35
36
  request: Request | None = None,
36
- parent_id: str | None = None,
37
+ parent_context: Context | None = None,
37
38
  start_time: datetime | None = None,
38
39
  end_time: datetime | None = None,
39
40
  span_kind: SpanKind | None = None,
@@ -45,8 +46,9 @@ class ExecuteToolScope(OpenTelemetryScope):
45
46
  agent_details: The details of the agent making the call
46
47
  tenant_details: The details of the tenant
47
48
  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.
50
52
  start_time: Optional explicit start time as a datetime object. Useful when
51
53
  recording a tool call after execution has already completed.
52
54
  end_time: Optional explicit end time as a datetime object. When provided,
@@ -63,7 +65,7 @@ class ExecuteToolScope(OpenTelemetryScope):
63
65
  agent_details,
64
66
  tenant_details,
65
67
  request,
66
- parent_id,
68
+ parent_context,
67
69
  start_time,
68
70
  end_time,
69
71
  span_kind,
@@ -75,7 +77,7 @@ class ExecuteToolScope(OpenTelemetryScope):
75
77
  agent_details: AgentDetails,
76
78
  tenant_details: TenantDetails,
77
79
  request: Request | None = None,
78
- parent_id: str | None = None,
80
+ parent_context: Context | None = None,
79
81
  start_time: datetime | None = None,
80
82
  end_time: datetime | None = None,
81
83
  span_kind: SpanKind | None = None,
@@ -87,8 +89,9 @@ class ExecuteToolScope(OpenTelemetryScope):
87
89
  agent_details: The details of the agent making the call
88
90
  tenant_details: The details of the tenant
89
91
  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.
92
95
  start_time: Optional explicit start time as a datetime object. Useful when
93
96
  recording a tool call after execution has already completed.
94
97
  end_time: Optional explicit end time as a datetime object. When provided,
@@ -103,7 +106,7 @@ class ExecuteToolScope(OpenTelemetryScope):
103
106
  activity_name=f"{EXECUTE_TOOL_OPERATION_NAME} {details.tool_name}",
104
107
  agent_details=agent_details,
105
108
  tenant_details=tenant_details,
106
- parent_id=parent_id,
109
+ parent_context=parent_context,
107
110
  start_time=start_time,
108
111
  end_time=end_time,
109
112
  )
@@ -23,6 +23,7 @@ from .utils import (
23
23
  hex_span_id,
24
24
  hex_trace_id,
25
25
  kind_name,
26
+ parse_retry_after,
26
27
  partition_by_identity,
27
28
  status_name,
28
29
  truncate_span,
@@ -79,9 +80,9 @@ class _Agent365Exporter(SpanExporter):
79
80
  logger.info("No spans with tenant/agent identity found; nothing exported.")
80
81
  return SpanExportResult.SUCCESS
81
82
 
82
- # Debug: Log number of groups and total span count
83
+ # Log number of groups and total span count
83
84
  total_spans = sum(len(activities) for activities in groups.values())
84
- logger.info(
85
+ logger.debug(
85
86
  f"Found {len(groups)} identity groups with {total_spans} total spans to export"
86
87
  )
87
88
 
@@ -98,8 +99,8 @@ class _Agent365Exporter(SpanExporter):
98
99
 
99
100
  url = build_export_url(endpoint, agent_id, tenant_id, self._use_s2s_endpoint)
100
101
 
101
- # Debug: Log endpoint being used
102
- logger.info(
102
+ # Log endpoint details at DEBUG to avoid leaking IDs in production logs
103
+ logger.debug(
103
104
  f"Exporting {len(activities)} spans to endpoint: {url} "
104
105
  f"(tenant: {tenant_id}, agent: {agent_id})"
105
106
  )
@@ -108,10 +109,16 @@ class _Agent365Exporter(SpanExporter):
108
109
  try:
109
110
  token = self._token_resolver(agent_id, tenant_id)
110
111
  if token:
112
+ # Warn if sending bearer token over non-HTTPS connection
113
+ if not url.lower().startswith("https://"):
114
+ logger.warning(
115
+ "Bearer token is being sent over a non-HTTPS connection. "
116
+ "This may expose credentials in transit."
117
+ )
111
118
  headers["authorization"] = f"Bearer {token}"
112
- logger.info(f"Token resolved successfully for agent {agent_id}")
119
+ logger.debug(f"Token resolved successfully for agent {agent_id}")
113
120
  else:
114
- logger.info(f"No token returned for agent {agent_id}")
121
+ logger.debug(f"No token returned for agent {agent_id}")
115
122
  except Exception as e:
116
123
  # If token resolution fails, treat as failure for this group
117
124
  logger.error(
@@ -174,7 +181,7 @@ class _Agent365Exporter(SpanExporter):
174
181
 
175
182
  # 2xx => success
176
183
  if 200 <= resp.status_code < 300:
177
- logger.info(
184
+ logger.debug(
178
185
  f"HTTP {resp.status_code} success on attempt {attempt + 1}. "
179
186
  f"Correlation ID: {correlation_id}. "
180
187
  f"Response: {self._truncate_text(resp.text, 200)}"
@@ -186,12 +193,19 @@ class _Agent365Exporter(SpanExporter):
186
193
 
187
194
  # Retry transient
188
195
  if resp.status_code in (408, 429) or 500 <= resp.status_code < 600:
196
+ # Respect Retry-After header for 429 responses
197
+ retry_after = parse_retry_after(resp.headers)
189
198
  if attempt < DEFAULT_MAX_RETRIES:
190
- time.sleep(0.2 * (attempt + 1))
199
+ if retry_after is not None:
200
+ time.sleep(min(retry_after, 60.0))
201
+ else:
202
+ # Exponential backoff with base 0.5s
203
+ time.sleep(0.5 * (2**attempt))
191
204
  continue
192
205
  # Final attempt failed
193
206
  logger.error(
194
- f"HTTP {resp.status_code} final failure after {DEFAULT_MAX_RETRIES + 1} attempts. "
207
+ f"HTTP {resp.status_code} final failure after "
208
+ f"{DEFAULT_MAX_RETRIES + 1} attempts. "
195
209
  f"Correlation ID: {correlation_id}. "
196
210
  f"Response: {response_text}"
197
211
  )
@@ -206,12 +220,11 @@ class _Agent365Exporter(SpanExporter):
206
220
 
207
221
  except requests.RequestException as e:
208
222
  if attempt < DEFAULT_MAX_RETRIES:
209
- time.sleep(0.2 * (attempt + 1))
223
+ # Exponential backoff with base 0.5s
224
+ time.sleep(0.5 * (2**attempt))
210
225
  continue
211
226
  # Final attempt failed
212
- logger.error(
213
- f"Request failed after {DEFAULT_MAX_RETRIES + 1} attempts with exception: {e}"
214
- )
227
+ logger.error(f"Request failed after {DEFAULT_MAX_RETRIES + 1} attempts: {e}")
215
228
  return False
216
229
  return False
217
230
 
@@ -1,6 +1,8 @@
1
1
  # Copyright (c) Microsoft Corporation.
2
2
  # Licensed under the MIT License.
3
3
 
4
+ from __future__ import annotations
5
+
4
6
  import json
5
7
  import logging
6
8
  import os
@@ -194,6 +196,13 @@ def get_validated_domain_override() -> str | None:
194
196
  logger.warning(f"Invalid domain override '{domain_override}': {e}")
195
197
  return None
196
198
 
199
+ # Warn when using insecure HTTP — telemetry data and bearer tokens may be exposed
200
+ if domain_override.lower().startswith("http://"):
201
+ logger.warning(
202
+ "Domain override uses insecure HTTP. Telemetry data (including "
203
+ "bearer tokens) will be transmitted in cleartext."
204
+ )
205
+
197
206
  return domain_override
198
207
 
199
208
 
@@ -223,6 +232,31 @@ def build_export_url(
223
232
  return f"https://{endpoint}{endpoint_path}?api-version=1"
224
233
 
225
234
 
235
+ def parse_retry_after(headers: dict[str, str]) -> float | None:
236
+ """Parse the ``Retry-After`` header value.
237
+
238
+ Only numeric (seconds) values are supported. HTTP-date values
239
+ (e.g. ``Wed, 21 Oct 2025 07:28:00 GMT``) are intentionally ignored
240
+ and treated as absent, falling back to exponential backoff.
241
+
242
+ Args:
243
+ headers: Response headers mapping.
244
+
245
+ Returns:
246
+ The number of seconds to wait, or ``None`` if the header is
247
+ absent, non-numeric, or otherwise invalid.
248
+ """
249
+ retry_after = headers.get("Retry-After")
250
+ if retry_after is None:
251
+ return None
252
+ try:
253
+ return float(retry_after)
254
+ except (ValueError, TypeError):
255
+ # Intentionally ignore HTTP-date formatted Retry-After values;
256
+ # callers should fall back to exponential backoff.
257
+ return None
258
+
259
+
226
260
  def is_agent365_exporter_enabled() -> bool:
227
261
  """Check if Agent 365 exporter is enabled."""
228
262
  # Check environment variable
@@ -4,6 +4,8 @@
4
4
  from datetime import datetime
5
5
  from typing import List
6
6
 
7
+ from opentelemetry.context import Context
8
+
7
9
  from .agent_details import AgentDetails
8
10
  from .constants import (
9
11
  CHANNEL_LINK_KEY,
@@ -36,7 +38,7 @@ class InferenceScope(OpenTelemetryScope):
36
38
  agent_details: AgentDetails,
37
39
  tenant_details: TenantDetails,
38
40
  request: Request | None = None,
39
- parent_id: str | None = None,
41
+ parent_context: Context | None = None,
40
42
  start_time: datetime | None = None,
41
43
  end_time: datetime | None = None,
42
44
  ) -> "InferenceScope":
@@ -47,8 +49,9 @@ class InferenceScope(OpenTelemetryScope):
47
49
  agent_details: The details of the agent making the call
48
50
  tenant_details: The details of the tenant
49
51
  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.
52
55
  start_time: Optional explicit start time as a datetime object.
53
56
  end_time: Optional explicit end time as a datetime object.
54
57
 
@@ -56,7 +59,7 @@ class InferenceScope(OpenTelemetryScope):
56
59
  A new InferenceScope instance
57
60
  """
58
61
  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
60
63
  )
61
64
 
62
65
  def __init__(
@@ -65,7 +68,7 @@ class InferenceScope(OpenTelemetryScope):
65
68
  agent_details: AgentDetails,
66
69
  tenant_details: TenantDetails,
67
70
  request: Request | None = None,
68
- parent_id: str | None = None,
71
+ parent_context: Context | None = None,
69
72
  start_time: datetime | None = None,
70
73
  end_time: datetime | None = None,
71
74
  ):
@@ -76,8 +79,9 @@ class InferenceScope(OpenTelemetryScope):
76
79
  agent_details: The details of the agent making the call
77
80
  tenant_details: The details of the tenant
78
81
  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.
81
85
  start_time: Optional explicit start time as a datetime object.
82
86
  end_time: Optional explicit end time as a datetime object.
83
87
  """
@@ -88,7 +92,7 @@ class InferenceScope(OpenTelemetryScope):
88
92
  activity_name=f"{details.operationName.value} {details.model}",
89
93
  agent_details=agent_details,
90
94
  tenant_details=tenant_details,
91
- parent_id=parent_id,
95
+ parent_context=parent_context,
92
96
  start_time=start_time,
93
97
  end_time=end_time,
94
98
  )
@@ -6,6 +6,7 @@
6
6
  import logging
7
7
  from datetime import datetime
8
8
 
9
+ from opentelemetry.context import Context
9
10
  from opentelemetry.trace import SpanKind
10
11
 
11
12
  from .agent_details import AgentDetails
@@ -50,6 +51,7 @@ class InvokeAgentScope(OpenTelemetryScope):
50
51
  request: Request | None = None,
51
52
  caller_agent_details: AgentDetails | None = None,
52
53
  caller_details: CallerDetails | None = None,
54
+ parent_context: Context | None = None,
53
55
  start_time: datetime | None = None,
54
56
  end_time: datetime | None = None,
55
57
  span_kind: SpanKind | None = None,
@@ -63,6 +65,9 @@ class InvokeAgentScope(OpenTelemetryScope):
63
65
  request: Optional request details for additional context
64
66
  caller_agent_details: Optional details of the caller agent
65
67
  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.
66
71
  start_time: Optional explicit start time as a datetime object.
67
72
  end_time: Optional explicit end time as a datetime object.
68
73
  span_kind: Optional span kind override. Defaults to ``SpanKind.CLIENT``.
@@ -77,6 +82,7 @@ class InvokeAgentScope(OpenTelemetryScope):
77
82
  request,
78
83
  caller_agent_details,
79
84
  caller_details,
85
+ parent_context,
80
86
  start_time,
81
87
  end_time,
82
88
  span_kind,
@@ -89,6 +95,7 @@ class InvokeAgentScope(OpenTelemetryScope):
89
95
  request: Request | None = None,
90
96
  caller_agent_details: AgentDetails | None = None,
91
97
  caller_details: CallerDetails | None = None,
98
+ parent_context: Context | None = None,
92
99
  start_time: datetime | None = None,
93
100
  end_time: datetime | None = None,
94
101
  span_kind: SpanKind | None = None,
@@ -101,6 +108,9 @@ class InvokeAgentScope(OpenTelemetryScope):
101
108
  request: Optional request details for additional context
102
109
  caller_agent_details: Optional details of the caller agent
103
110
  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.
104
114
  start_time: Optional explicit start time as a datetime object.
105
115
  end_time: Optional explicit end time as a datetime object.
106
116
  span_kind: Optional span kind override. Defaults to ``SpanKind.CLIENT``.
@@ -118,6 +128,7 @@ class InvokeAgentScope(OpenTelemetryScope):
118
128
  activity_name=activity_name,
119
129
  agent_details=invoke_agent_details.details,
120
130
  tenant_details=tenant_details,
131
+ parent_context=parent_context,
121
132
  start_time=start_time,
122
133
  end_time=end_time,
123
134
  )
@@ -9,7 +9,9 @@ from datetime import datetime
9
9
  from threading import Lock
10
10
  from typing import TYPE_CHECKING, Any
11
11
 
12
- from opentelemetry import baggage, context, trace
12
+ from opentelemetry import context, trace
13
+ from opentelemetry.context import Context
14
+ from opentelemetry.propagate import inject
13
15
  from opentelemetry.trace import (
14
16
  Span,
15
17
  SpanKind,
@@ -43,7 +45,7 @@ from .constants import (
43
45
  TELEMETRY_SDK_VERSION_KEY,
44
46
  TENANT_ID_KEY,
45
47
  )
46
- from .utils import get_sdk_version, parse_parent_id_to_context
48
+ from .utils import get_sdk_version
47
49
 
48
50
  if TYPE_CHECKING:
49
51
  from .agent_details import AgentDetails
@@ -97,7 +99,7 @@ class OpenTelemetryScope:
97
99
  activity_name: str,
98
100
  agent_details: "AgentDetails | None" = None,
99
101
  tenant_details: "TenantDetails | None" = None,
100
- parent_id: str | None = None,
102
+ parent_context: Context | None = None,
101
103
  start_time: datetime | None = None,
102
104
  end_time: datetime | None = None,
103
105
  ):
@@ -111,8 +113,9 @@ class OpenTelemetryScope:
111
113
  activity_name: The name of the activity for display purposes
112
114
  agent_details: Optional agent details
113
115
  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.
116
119
  start_time: Optional explicit start time as a datetime object.
117
120
  Useful when recording an operation after it has already completed.
118
121
  end_time: Optional explicit end time as a datetime object.
@@ -146,9 +149,8 @@ class OpenTelemetryScope:
146
149
  activity_kind = SpanKind.CONSUMER
147
150
 
148
151
  # 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
150
153
  # Otherwise, use the current context
151
- parent_context = parse_parent_id_to_context(parent_id)
152
154
  span_context = parent_context if parent_context else context.get_current()
153
155
 
154
156
  # Convert custom start time to OTel-compatible format (nanoseconds since epoch)
@@ -241,21 +243,6 @@ class OpenTelemetryScope:
241
243
  if value is not None and self._span and self._is_telemetry_enabled():
242
244
  self._span.set_attribute(name, value)
243
245
 
244
- def add_baggage(self, key: str, value: str) -> None:
245
- """Add baggage to the current context.
246
-
247
- Args:
248
- key: The baggage key
249
- value: The baggage value
250
- """
251
- # Set baggage in the current context
252
- if self._is_telemetry_enabled():
253
- # Set baggage on the current context
254
- # This will be inherited by child spans created within this context
255
- baggage_context = baggage.set_baggage(key, value)
256
- # The context needs to be made current for child spans to inherit the baggage
257
- context.attach(baggage_context)
258
-
259
246
  def record_attributes(self, attributes: dict[str, Any] | list[tuple[str, Any]]) -> None:
260
247
  """Record multiple attribute key/value pairs for telemetry tracking.
261
248
 
@@ -301,6 +288,49 @@ class OpenTelemetryScope:
301
288
  else:
302
289
  self._span.end()
303
290
 
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
+
304
334
  def __enter__(self):
305
335
  """Enter the context manager and make span active."""
306
336
  if self._span and self._is_telemetry_enabled():
@@ -3,6 +3,8 @@
3
3
 
4
4
  from datetime import datetime
5
5
 
6
+ from opentelemetry.context import Context
7
+
6
8
  from ..agent_details import AgentDetails
7
9
  from ..constants import GEN_AI_OUTPUT_MESSAGES_KEY
8
10
  from ..models.response import Response
@@ -16,12 +18,14 @@ OUTPUT_OPERATION_NAME = "output_messages"
16
18
  class OutputScope(OpenTelemetryScope):
17
19
  """Provides OpenTelemetry tracing scope for output messages."""
18
20
 
21
+ _MAX_OUTPUT_MESSAGES = 5000
22
+
19
23
  @staticmethod
20
24
  def start(
21
25
  agent_details: AgentDetails,
22
26
  tenant_details: TenantDetails,
23
27
  response: Response,
24
- parent_id: str | None = None,
28
+ parent_context: Context | None = None,
25
29
  start_time: datetime | None = None,
26
30
  end_time: datetime | None = None,
27
31
  ) -> "OutputScope":
@@ -31,22 +35,25 @@ class OutputScope(OpenTelemetryScope):
31
35
  agent_details: The details of the agent
32
36
  tenant_details: The details of the tenant
33
37
  response: The response details from the agent
34
- parent_id: Optional parent Activity ID used to link this span to an upstream
35
- 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.
36
41
  start_time: Optional explicit start time as a datetime object.
37
42
  end_time: Optional explicit end time as a datetime object.
38
43
 
39
44
  Returns:
40
45
  A new OutputScope instance
41
46
  """
42
- 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
+ )
43
50
 
44
51
  def __init__(
45
52
  self,
46
53
  agent_details: AgentDetails,
47
54
  tenant_details: TenantDetails,
48
55
  response: Response,
49
- parent_id: str | None = None,
56
+ parent_context: Context | None = None,
50
57
  start_time: datetime | None = None,
51
58
  end_time: datetime | None = None,
52
59
  ):
@@ -56,8 +63,9 @@ class OutputScope(OpenTelemetryScope):
56
63
  agent_details: The details of the agent
57
64
  tenant_details: The details of the tenant
58
65
  response: The response details from the agent
59
- parent_id: Optional parent Activity ID used to link this span to an upstream
60
- 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.
61
69
  start_time: Optional explicit start time as a datetime object.
62
70
  end_time: Optional explicit end time as a datetime object.
63
71
  """
@@ -67,7 +75,7 @@ class OutputScope(OpenTelemetryScope):
67
75
  activity_name=(f"{OUTPUT_OPERATION_NAME} {agent_details.agent_id}"),
68
76
  agent_details=agent_details,
69
77
  tenant_details=tenant_details,
70
- parent_id=parent_id,
78
+ parent_context=parent_context,
71
79
  start_time=start_time,
72
80
  end_time=end_time,
73
81
  )
@@ -82,9 +90,12 @@ class OutputScope(OpenTelemetryScope):
82
90
  """Records the output messages for telemetry tracking.
83
91
 
84
92
  Appends the provided messages to the accumulated output messages list.
93
+ The list is capped at _MAX_OUTPUT_MESSAGES to prevent unbounded memory growth.
85
94
 
86
95
  Args:
87
96
  messages: List of output messages to append
88
97
  """
89
98
  self._output_messages.extend(messages)
99
+ if len(self._output_messages) > self._MAX_OUTPUT_MESSAGES:
100
+ self._output_messages = self._output_messages[-self._MAX_OUTPUT_MESSAGES :]
90
101
  self.set_tag_maybe(GEN_AI_OUTPUT_MESSAGES_KEY, safe_json_dumps(self._output_messages))
@@ -15,11 +15,12 @@ from threading import RLock
15
15
  from typing import Any, Generic, TypeVar, cast
16
16
 
17
17
  from opentelemetry import context
18
+ from opentelemetry.propagate import extract
18
19
  from opentelemetry.semconv.attributes.exception_attributes import (
19
20
  EXCEPTION_MESSAGE,
20
21
  EXCEPTION_STACKTRACE,
21
22
  )
22
- from opentelemetry.trace import NonRecordingSpan, Span, SpanContext, TraceFlags, set_span_in_context
23
+ from opentelemetry.trace import Span
23
24
  from opentelemetry.util.types import AttributeValue
24
25
  from wrapt import ObjectProxy
25
26
 
@@ -29,126 +30,58 @@ logger = logging.getLogger(__name__)
29
30
  logger.addHandler(logging.NullHandler())
30
31
 
31
32
 
32
- # W3C Trace Context constants
33
- W3C_TRACE_CONTEXT_VERSION = "00"
34
- W3C_TRACE_ID_LENGTH = 32 # 32 hex chars = 128 bits
35
- W3C_SPAN_ID_LENGTH = 16 # 16 hex chars = 64 bits
33
+ def extract_context_from_headers(headers: dict[str, str]) -> context.Context:
34
+ """Extract an OpenTelemetry Context from W3C trace HTTP headers.
36
35
 
37
-
38
- def validate_w3c_trace_context_version(version: str) -> bool:
39
- """Validate W3C Trace Context version.
36
+ Parses ``traceparent`` (and optionally ``tracestate``) headers and returns
37
+ an OpenTelemetry Context that can be passed as ``parent_context`` to any
38
+ scope's ``start()`` method.
40
39
 
41
40
  Args:
42
- version: The version string to validate
41
+ headers: Dictionary of HTTP headers containing trace context.
42
+ Expected keys include ``traceparent`` and optionally ``tracestate``.
43
43
 
44
44
  Returns:
45
- True if valid, False otherwise
46
- """
47
- return version == W3C_TRACE_CONTEXT_VERSION
45
+ An OpenTelemetry Context containing the extracted trace information.
46
+ If no valid trace context is found, returns an empty context.
48
47
 
48
+ Example::
49
49
 
50
- def _is_valid_hex(hex_string: str) -> bool:
51
- """Check if a string contains only valid hexadecimal characters.
52
-
53
- Args:
54
- hex_string: The string to validate
50
+ .. code-block:: python
55
51
 
56
- Returns:
57
- True if all characters are valid hexadecimal (0-9, a-f, A-F), False otherwise
52
+ headers = {
53
+ "traceparent": "00-1234567890abcdef1234567890abcdef-abcdefabcdef1234-01"
54
+ }
55
+ parent_context = extract_context_from_headers(headers)
56
+ with InferenceScope.start(
57
+ details, agent, tenant, parent_context=parent_context
58
+ ):
59
+ pass
58
60
  """
59
- return all(c in "0123456789abcdefABCDEF" for c in hex_string)
61
+ return extract(headers)
60
62
 
61
63
 
62
- def validate_trace_id(trace_id_hex: str) -> bool:
63
- """Validate W3C Trace Context trace_id format.
64
+ def get_traceparent(headers: dict[str, str]) -> str | None:
65
+ """Return the W3C ``traceparent`` value from a headers dictionary.
64
66
 
65
67
  Args:
66
- trace_id_hex: The trace_id hex string to validate (should be 32 hex chars)
68
+ headers: Dictionary of HTTP headers, typically obtained from
69
+ :meth:`OpenTelemetryScope.inject_context_to_headers`.
67
70
 
68
71
  Returns:
69
- True if valid (32 hex chars), False otherwise
70
- """
71
- return len(trace_id_hex) == W3C_TRACE_ID_LENGTH and _is_valid_hex(trace_id_hex)
72
+ The traceparent string (e.g.
73
+ ``"00-<trace-id>-<span-id>-<flags>"``), or ``None`` if the
74
+ key is not present.
72
75
 
76
+ Example::
73
77
 
74
- def validate_span_id(span_id_hex: str) -> bool:
75
- """Validate W3C Trace Context span_id format.
76
-
77
- Args:
78
- span_id_hex: The span_id hex string to validate (should be 16 hex chars)
78
+ .. code-block:: python
79
79
 
80
- Returns:
81
- True if valid (16 hex chars), False otherwise
80
+ # Extract traceparent from incoming HTTP request headers
81
+ traceparent = get_traceparent(request.headers)
82
+ turn_context.turn_state[A365_PARENT_TRACEPARENT_KEY] = traceparent
82
83
  """
83
- return len(span_id_hex) == W3C_SPAN_ID_LENGTH and _is_valid_hex(span_id_hex)
84
-
85
-
86
- def parse_parent_id_to_context(parent_id: str | None) -> context.Context | None:
87
- """Parse a W3C trace context parent ID and return a context with the parent span.
88
-
89
- The parent_id format is expected to be W3C Trace Context format:
90
- "00-{trace_id}-{span_id}-{trace_flags}"
91
- Example: "00-1234567890abcdef1234567890abcdef-abcdefabcdef1234-01"
92
-
93
- Args:
94
- parent_id: The W3C Trace Context format parent ID string
95
-
96
- Returns:
97
- A context containing the parent span, or None if parent_id is invalid
98
- """
99
- if not parent_id:
100
- return None
101
-
102
- try:
103
- # W3C Trace Context format: "00-{trace_id}-{span_id}-{trace_flags}"
104
- parts = parent_id.split("-")
105
- if len(parts) != 4:
106
- logger.warning(f"Invalid parent_id format (expected 4 parts): {parent_id}")
107
- return None
108
-
109
- version, trace_id_hex, span_id_hex, trace_flags_hex = parts
110
-
111
- # Validate W3C Trace Context version
112
- if not validate_w3c_trace_context_version(version):
113
- logger.warning(f"Unsupported W3C Trace Context version: {version}")
114
- return None
115
-
116
- # Validate trace_id (must be 32 hex chars)
117
- if not validate_trace_id(trace_id_hex):
118
- logger.warning(
119
- f"Invalid trace_id (expected {W3C_TRACE_ID_LENGTH} hex chars): '{trace_id_hex}'"
120
- )
121
- return None
122
-
123
- # Validate span_id (must be 16 hex chars)
124
- if not validate_span_id(span_id_hex):
125
- logger.warning(
126
- f"Invalid span_id (expected {W3C_SPAN_ID_LENGTH} hex chars): '{span_id_hex}'"
127
- )
128
- return None
129
-
130
- # Parse the hex values
131
- trace_id = int(trace_id_hex, 16)
132
- span_id = int(span_id_hex, 16)
133
- trace_flags = TraceFlags(int(trace_flags_hex, 16))
134
-
135
- # Create a SpanContext from the parsed values
136
- parent_span_context = SpanContext(
137
- trace_id=trace_id,
138
- span_id=span_id,
139
- is_remote=True,
140
- trace_flags=trace_flags,
141
- )
142
-
143
- # Create a NonRecordingSpan with the parent context
144
- parent_span = NonRecordingSpan(parent_span_context)
145
-
146
- # Create a context with the parent span
147
- return set_span_in_context(parent_span)
148
-
149
- except (ValueError, IndexError) as e:
150
- logger.warning(f"Failed to parse parent_id '{parent_id}': {e}")
151
- return None
84
+ return headers.get("traceparent")
152
85
 
153
86
 
154
87
  def safe_json_dumps(obj: Any, **kwargs: Any) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-observability-core
3
- Version: 0.2.1.dev36
3
+ Version: 0.2.1.dev43
4
4
  Summary: Telemetry, tracing, and monitoring components for AI agents
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT