microsoft-agents-a365-observability-core 0.2.1.dev36__tar.gz → 0.2.1.dev42__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.
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/PKG-INFO +1 -1
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py +26 -13
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/exporters/utils.py +34 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/opentelemetry_scope.py +1 -16
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/spans_scopes/output_scope.py +5 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365_observability_core.egg-info/PKG-INFO +1 -1
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/README.md +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/__init__.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/agent_details.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/config.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/constants.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/execute_tool_scope.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/execution_type.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/exporters/__init__.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/exporters/agent365_exporter_options.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/exporters/enriched_span.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/exporters/enriching_span_processor.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/exporters/spectra_exporter_options.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/inference_call_details.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/inference_operation_type.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/inference_scope.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/invoke_agent_details.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/invoke_agent_scope.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/middleware/__init__.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/middleware/baggage_builder.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/models/__init__.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/models/agent_type.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/models/caller_details.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/models/operation_source.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/models/response.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/request.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/source_metadata.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/spans_scopes/__init__.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/tenant_details.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/tool_call_details.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/tool_type.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/trace_processor/__init__.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/trace_processor/span_processor.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/trace_processor/util.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/utils.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365_observability_core.egg-info/SOURCES.txt +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365_observability_core.egg-info/dependency_links.txt +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365_observability_core.egg-info/requires.txt +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365_observability_core.egg-info/top_level.txt +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/pyproject.toml +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/setup.cfg +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev36 → microsoft_agents_a365_observability_core-0.2.1.dev42}/setup.py +0 -0
|
@@ -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
|
-
#
|
|
83
|
+
# Log number of groups and total span count
|
|
83
84
|
total_spans = sum(len(activities) for activities in groups.values())
|
|
84
|
-
logger.
|
|
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
|
-
#
|
|
102
|
-
logger.
|
|
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.
|
|
119
|
+
logger.debug(f"Token resolved successfully for agent {agent_id}")
|
|
113
120
|
else:
|
|
114
|
-
logger.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -9,7 +9,7 @@ from datetime import datetime
|
|
|
9
9
|
from threading import Lock
|
|
10
10
|
from typing import TYPE_CHECKING, Any
|
|
11
11
|
|
|
12
|
-
from opentelemetry import
|
|
12
|
+
from opentelemetry import context, trace
|
|
13
13
|
from opentelemetry.trace import (
|
|
14
14
|
Span,
|
|
15
15
|
SpanKind,
|
|
@@ -241,21 +241,6 @@ class OpenTelemetryScope:
|
|
|
241
241
|
if value is not None and self._span and self._is_telemetry_enabled():
|
|
242
242
|
self._span.set_attribute(name, value)
|
|
243
243
|
|
|
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
244
|
def record_attributes(self, attributes: dict[str, Any] | list[tuple[str, Any]]) -> None:
|
|
260
245
|
"""Record multiple attribute key/value pairs for telemetry tracking.
|
|
261
246
|
|
|
@@ -16,6 +16,8 @@ OUTPUT_OPERATION_NAME = "output_messages"
|
|
|
16
16
|
class OutputScope(OpenTelemetryScope):
|
|
17
17
|
"""Provides OpenTelemetry tracing scope for output messages."""
|
|
18
18
|
|
|
19
|
+
_MAX_OUTPUT_MESSAGES = 5000
|
|
20
|
+
|
|
19
21
|
@staticmethod
|
|
20
22
|
def start(
|
|
21
23
|
agent_details: AgentDetails,
|
|
@@ -82,9 +84,12 @@ class OutputScope(OpenTelemetryScope):
|
|
|
82
84
|
"""Records the output messages for telemetry tracking.
|
|
83
85
|
|
|
84
86
|
Appends the provided messages to the accumulated output messages list.
|
|
87
|
+
The list is capped at _MAX_OUTPUT_MESSAGES to prevent unbounded memory growth.
|
|
85
88
|
|
|
86
89
|
Args:
|
|
87
90
|
messages: List of output messages to append
|
|
88
91
|
"""
|
|
89
92
|
self._output_messages.extend(messages)
|
|
93
|
+
if len(self._output_messages) > self._MAX_OUTPUT_MESSAGES:
|
|
94
|
+
self._output_messages = self._output_messages[-self._MAX_OUTPUT_MESSAGES :]
|
|
90
95
|
self.set_tag_maybe(GEN_AI_OUTPUT_MESSAGES_KEY, safe_json_dumps(self._output_messages))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|