microsoft-agents-a365-observability-core 0.2.1.dev35__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.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/PKG-INFO +1 -1
- {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/__init__.py +5 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/config.py +39 -12
- {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/exporters/__init__.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py +26 -33
- {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/exporters/enriched_span.py +10 -1
- {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365/observability/core/exporters/enriching_span_processor.py +30 -0
- microsoft_agents_a365_observability_core-0.2.1.dev42/microsoft_agents_a365/observability/core/exporters/spectra_exporter_options.py +56 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/microsoft_agents_a365_observability_core.egg-info/SOURCES.txt +1 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/README.md +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → 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.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/pyproject.toml +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/setup.cfg +0 -0
- {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/setup.py +0 -0
|
@@ -12,12 +12,14 @@ from .config import (
|
|
|
12
12
|
)
|
|
13
13
|
from .execute_tool_scope import ExecuteToolScope
|
|
14
14
|
from .execution_type import ExecutionType
|
|
15
|
+
from .exporters.agent365_exporter_options import Agent365ExporterOptions
|
|
15
16
|
from .exporters.enriched_span import EnrichedReadableSpan
|
|
16
17
|
from .exporters.enriching_span_processor import (
|
|
17
18
|
get_span_enricher,
|
|
18
19
|
register_span_enricher,
|
|
19
20
|
unregister_span_enricher,
|
|
20
21
|
)
|
|
22
|
+
from .exporters.spectra_exporter_options import SpectraExporterOptions
|
|
21
23
|
from .inference_call_details import InferenceCallDetails, ServiceEndpoint
|
|
22
24
|
from .inference_operation_type import InferenceOperationType
|
|
23
25
|
from .inference_scope import InferenceScope
|
|
@@ -38,6 +40,9 @@ __all__ = [
|
|
|
38
40
|
"is_configured",
|
|
39
41
|
"get_tracer",
|
|
40
42
|
"get_tracer_provider",
|
|
43
|
+
# Exporter options
|
|
44
|
+
"Agent365ExporterOptions",
|
|
45
|
+
"SpectraExporterOptions",
|
|
41
46
|
# Span enrichment
|
|
42
47
|
"register_span_enricher",
|
|
43
48
|
"unregister_span_enricher",
|
|
@@ -8,6 +8,9 @@ from collections.abc import Callable
|
|
|
8
8
|
from typing import Any, Optional
|
|
9
9
|
|
|
10
10
|
from opentelemetry import trace
|
|
11
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
|
12
|
+
OTLPSpanExporter as GrpcOTLPSpanExporter,
|
|
13
|
+
)
|
|
11
14
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
12
15
|
from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_NAMESPACE, Resource
|
|
13
16
|
from opentelemetry.sdk.trace import TracerProvider
|
|
@@ -18,6 +21,7 @@ from .exporters.agent365_exporter_options import Agent365ExporterOptions
|
|
|
18
21
|
from .exporters.enriching_span_processor import (
|
|
19
22
|
_EnrichingBatchSpanProcessor,
|
|
20
23
|
)
|
|
24
|
+
from .exporters.spectra_exporter_options import SpectraExporterOptions
|
|
21
25
|
from .exporters.utils import is_agent365_exporter_enabled
|
|
22
26
|
from .trace_processor.span_processor import SpanProcessor
|
|
23
27
|
|
|
@@ -58,7 +62,7 @@ class TelemetryManager:
|
|
|
58
62
|
logger_name: str = DEFAULT_LOGGER_NAME,
|
|
59
63
|
token_resolver: Callable[[str, str], str | None] | None = None,
|
|
60
64
|
cluster_category: str = "prod",
|
|
61
|
-
exporter_options:
|
|
65
|
+
exporter_options: Agent365ExporterOptions | SpectraExporterOptions | None = None,
|
|
62
66
|
suppress_invoke_agent_input: bool = False,
|
|
63
67
|
**kwargs: Any,
|
|
64
68
|
) -> bool:
|
|
@@ -72,9 +76,10 @@ class TelemetryManager:
|
|
|
72
76
|
Use exporter_options instead.
|
|
73
77
|
:param cluster_category: (Deprecated) Environment / cluster category (e.g. "prod").
|
|
74
78
|
Use exporter_options instead.
|
|
75
|
-
:param exporter_options: Agent365ExporterOptions
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
:param exporter_options: Exporter configuration. Pass Agent365ExporterOptions for A365 API
|
|
80
|
+
export, SpectraExporterOptions for Spectra Collector sidecar export, or None (default)
|
|
81
|
+
to construct Agent365ExporterOptions from legacy parameters.
|
|
82
|
+
:param suppress_invoke_agent_input: If True, suppress input messages for InvokeAgent spans.
|
|
78
83
|
:return: True if configuration succeeded, False otherwise.
|
|
79
84
|
"""
|
|
80
85
|
try:
|
|
@@ -100,7 +105,7 @@ class TelemetryManager:
|
|
|
100
105
|
logger_name: str,
|
|
101
106
|
token_resolver: Callable[[str, str], str | None] | None = None,
|
|
102
107
|
cluster_category: str = "prod",
|
|
103
|
-
exporter_options:
|
|
108
|
+
exporter_options: Agent365ExporterOptions | SpectraExporterOptions | None = None,
|
|
104
109
|
suppress_invoke_agent_input: bool = False,
|
|
105
110
|
**kwargs: Any,
|
|
106
111
|
) -> bool:
|
|
@@ -156,25 +161,43 @@ class TelemetryManager:
|
|
|
156
161
|
"max_export_batch_size": exporter_options.max_export_batch_size,
|
|
157
162
|
}
|
|
158
163
|
|
|
159
|
-
|
|
164
|
+
# Type-based exporter dispatch
|
|
165
|
+
if isinstance(exporter_options, SpectraExporterOptions):
|
|
166
|
+
# Spectra path — OTLP exporter to sidecar
|
|
167
|
+
# ENABLE_A365_OBSERVABILITY_EXPORTER is intentionally ignored.
|
|
168
|
+
if exporter_options.protocol == "grpc":
|
|
169
|
+
exporter = GrpcOTLPSpanExporter(
|
|
170
|
+
endpoint=exporter_options.endpoint,
|
|
171
|
+
insecure=exporter_options.insecure,
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
exporter = OTLPSpanExporter(
|
|
175
|
+
endpoint=exporter_options.endpoint,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
elif is_agent365_exporter_enabled() and exporter_options.token_resolver is not None:
|
|
160
179
|
exporter = _Agent365Exporter(
|
|
161
180
|
token_resolver=exporter_options.token_resolver,
|
|
162
181
|
cluster_category=exporter_options.cluster_category,
|
|
163
182
|
use_s2s_endpoint=exporter_options.use_s2s_endpoint,
|
|
164
|
-
suppress_invoke_agent_input=suppress_invoke_agent_input,
|
|
165
183
|
)
|
|
166
184
|
|
|
167
185
|
else:
|
|
168
186
|
exporter = ConsoleSpanExporter()
|
|
169
187
|
self._logger.warning(
|
|
170
|
-
"is_agent365_exporter_enabled() not enabled or token_resolver not set.
|
|
188
|
+
"is_agent365_exporter_enabled() not enabled or token_resolver not set."
|
|
189
|
+
" Falling back to console exporter."
|
|
171
190
|
)
|
|
172
191
|
|
|
173
192
|
# Add span processors
|
|
174
193
|
|
|
175
194
|
# Create _EnrichingBatchSpanProcessor with optimized settings
|
|
176
195
|
# This allows extensions to enrich spans before export
|
|
177
|
-
batch_processor = _EnrichingBatchSpanProcessor(
|
|
196
|
+
batch_processor = _EnrichingBatchSpanProcessor(
|
|
197
|
+
exporter,
|
|
198
|
+
suppress_invoke_agent_input=suppress_invoke_agent_input,
|
|
199
|
+
**batch_processor_kwargs,
|
|
200
|
+
)
|
|
178
201
|
agent_processor = SpanProcessor()
|
|
179
202
|
|
|
180
203
|
tracer_provider.add_span_processor(batch_processor)
|
|
@@ -248,7 +271,8 @@ def configure(
|
|
|
248
271
|
logger_name: str = DEFAULT_LOGGER_NAME,
|
|
249
272
|
token_resolver: Callable[[str, str], str | None] | None = None,
|
|
250
273
|
cluster_category: str = "prod",
|
|
251
|
-
exporter_options:
|
|
274
|
+
exporter_options: Agent365ExporterOptions | SpectraExporterOptions | None = None,
|
|
275
|
+
suppress_invoke_agent_input: bool = False,
|
|
252
276
|
**kwargs: Any,
|
|
253
277
|
) -> bool:
|
|
254
278
|
"""
|
|
@@ -261,8 +285,10 @@ def configure(
|
|
|
261
285
|
Use exporter_options instead.
|
|
262
286
|
:param cluster_category: (Deprecated) Environment / cluster category (e.g. "prod").
|
|
263
287
|
Use exporter_options instead.
|
|
264
|
-
:param exporter_options: Agent365ExporterOptions
|
|
265
|
-
|
|
288
|
+
:param exporter_options: Exporter configuration. Pass Agent365ExporterOptions for A365 API
|
|
289
|
+
export, SpectraExporterOptions for Spectra Collector sidecar export, or None (default)
|
|
290
|
+
to construct Agent365ExporterOptions from legacy parameters.
|
|
291
|
+
:param suppress_invoke_agent_input: If True, suppress input messages for InvokeAgent spans.
|
|
266
292
|
:return: True if configuration succeeded, False otherwise.
|
|
267
293
|
"""
|
|
268
294
|
return _telemetry_manager.configure(
|
|
@@ -272,6 +298,7 @@ def configure(
|
|
|
272
298
|
token_resolver,
|
|
273
299
|
cluster_category,
|
|
274
300
|
exporter_options,
|
|
301
|
+
suppress_invoke_agent_input,
|
|
275
302
|
**kwargs,
|
|
276
303
|
)
|
|
277
304
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
# Licensed under the MIT License.
|
|
3
3
|
|
|
4
4
|
from .agent365_exporter_options import Agent365ExporterOptions
|
|
5
|
+
from .spectra_exporter_options import SpectraExporterOptions
|
|
5
6
|
|
|
6
7
|
# Agent365Exporter is not exported intentionally.
|
|
7
8
|
# It should only be used internally by the observability core module.
|
|
8
|
-
__all__ = ["Agent365ExporterOptions"]
|
|
9
|
+
__all__ = ["Agent365ExporterOptions", "SpectraExporterOptions"]
|
|
@@ -17,17 +17,13 @@ from opentelemetry.sdk.trace import ReadableSpan
|
|
|
17
17
|
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
18
18
|
from opentelemetry.trace import StatusCode
|
|
19
19
|
|
|
20
|
-
from ..constants import (
|
|
21
|
-
GEN_AI_INPUT_MESSAGES_KEY,
|
|
22
|
-
GEN_AI_OPERATION_NAME_KEY,
|
|
23
|
-
INVOKE_AGENT_OPERATION_NAME,
|
|
24
|
-
)
|
|
25
20
|
from .utils import (
|
|
26
21
|
build_export_url,
|
|
27
22
|
get_validated_domain_override,
|
|
28
23
|
hex_span_id,
|
|
29
24
|
hex_trace_id,
|
|
30
25
|
kind_name,
|
|
26
|
+
parse_retry_after,
|
|
31
27
|
partition_by_identity,
|
|
32
28
|
status_name,
|
|
33
29
|
truncate_span,
|
|
@@ -59,7 +55,6 @@ class _Agent365Exporter(SpanExporter):
|
|
|
59
55
|
token_resolver: Callable[[str, str], str | None],
|
|
60
56
|
cluster_category: str = "prod",
|
|
61
57
|
use_s2s_endpoint: bool = False,
|
|
62
|
-
suppress_invoke_agent_input: bool = False,
|
|
63
58
|
):
|
|
64
59
|
if token_resolver is None:
|
|
65
60
|
raise ValueError("token_resolver must be provided.")
|
|
@@ -69,7 +64,6 @@ class _Agent365Exporter(SpanExporter):
|
|
|
69
64
|
self._token_resolver = token_resolver
|
|
70
65
|
self._cluster_category = cluster_category
|
|
71
66
|
self._use_s2s_endpoint = use_s2s_endpoint
|
|
72
|
-
self._suppress_invoke_agent_input = suppress_invoke_agent_input
|
|
73
67
|
# Read domain override once at initialization
|
|
74
68
|
self._domain_override = get_validated_domain_override()
|
|
75
69
|
|
|
@@ -86,9 +80,9 @@ class _Agent365Exporter(SpanExporter):
|
|
|
86
80
|
logger.info("No spans with tenant/agent identity found; nothing exported.")
|
|
87
81
|
return SpanExportResult.SUCCESS
|
|
88
82
|
|
|
89
|
-
#
|
|
83
|
+
# Log number of groups and total span count
|
|
90
84
|
total_spans = sum(len(activities) for activities in groups.values())
|
|
91
|
-
logger.
|
|
85
|
+
logger.debug(
|
|
92
86
|
f"Found {len(groups)} identity groups with {total_spans} total spans to export"
|
|
93
87
|
)
|
|
94
88
|
|
|
@@ -105,8 +99,8 @@ class _Agent365Exporter(SpanExporter):
|
|
|
105
99
|
|
|
106
100
|
url = build_export_url(endpoint, agent_id, tenant_id, self._use_s2s_endpoint)
|
|
107
101
|
|
|
108
|
-
#
|
|
109
|
-
logger.
|
|
102
|
+
# Log endpoint details at DEBUG to avoid leaking IDs in production logs
|
|
103
|
+
logger.debug(
|
|
110
104
|
f"Exporting {len(activities)} spans to endpoint: {url} "
|
|
111
105
|
f"(tenant: {tenant_id}, agent: {agent_id})"
|
|
112
106
|
)
|
|
@@ -115,10 +109,16 @@ class _Agent365Exporter(SpanExporter):
|
|
|
115
109
|
try:
|
|
116
110
|
token = self._token_resolver(agent_id, tenant_id)
|
|
117
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
|
+
)
|
|
118
118
|
headers["authorization"] = f"Bearer {token}"
|
|
119
|
-
logger.
|
|
119
|
+
logger.debug(f"Token resolved successfully for agent {agent_id}")
|
|
120
120
|
else:
|
|
121
|
-
logger.
|
|
121
|
+
logger.debug(f"No token returned for agent {agent_id}")
|
|
122
122
|
except Exception as e:
|
|
123
123
|
# If token resolution fails, treat as failure for this group
|
|
124
124
|
logger.error(
|
|
@@ -181,7 +181,7 @@ class _Agent365Exporter(SpanExporter):
|
|
|
181
181
|
|
|
182
182
|
# 2xx => success
|
|
183
183
|
if 200 <= resp.status_code < 300:
|
|
184
|
-
logger.
|
|
184
|
+
logger.debug(
|
|
185
185
|
f"HTTP {resp.status_code} success on attempt {attempt + 1}. "
|
|
186
186
|
f"Correlation ID: {correlation_id}. "
|
|
187
187
|
f"Response: {self._truncate_text(resp.text, 200)}"
|
|
@@ -193,12 +193,19 @@ class _Agent365Exporter(SpanExporter):
|
|
|
193
193
|
|
|
194
194
|
# Retry transient
|
|
195
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)
|
|
196
198
|
if attempt < DEFAULT_MAX_RETRIES:
|
|
197
|
-
|
|
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))
|
|
198
204
|
continue
|
|
199
205
|
# Final attempt failed
|
|
200
206
|
logger.error(
|
|
201
|
-
f"HTTP {resp.status_code} final failure after
|
|
207
|
+
f"HTTP {resp.status_code} final failure after "
|
|
208
|
+
f"{DEFAULT_MAX_RETRIES + 1} attempts. "
|
|
202
209
|
f"Correlation ID: {correlation_id}. "
|
|
203
210
|
f"Response: {response_text}"
|
|
204
211
|
)
|
|
@@ -213,12 +220,11 @@ class _Agent365Exporter(SpanExporter):
|
|
|
213
220
|
|
|
214
221
|
except requests.RequestException as e:
|
|
215
222
|
if attempt < DEFAULT_MAX_RETRIES:
|
|
216
|
-
|
|
223
|
+
# Exponential backoff with base 0.5s
|
|
224
|
+
time.sleep(0.5 * (2**attempt))
|
|
217
225
|
continue
|
|
218
226
|
# Final attempt failed
|
|
219
|
-
logger.error(
|
|
220
|
-
f"Request failed after {DEFAULT_MAX_RETRIES + 1} attempts with exception: {e}"
|
|
221
|
-
)
|
|
227
|
+
logger.error(f"Request failed after {DEFAULT_MAX_RETRIES + 1} attempts: {e}")
|
|
222
228
|
return False
|
|
223
229
|
return False
|
|
224
230
|
|
|
@@ -270,19 +276,6 @@ class _Agent365Exporter(SpanExporter):
|
|
|
270
276
|
# attributes
|
|
271
277
|
attrs = dict(sp.attributes or {})
|
|
272
278
|
|
|
273
|
-
# Suppress input messages if configured and current span is an InvokeAgent span
|
|
274
|
-
if self._suppress_invoke_agent_input:
|
|
275
|
-
# Check if current span is an InvokeAgent span by:
|
|
276
|
-
# 1. Span name starts with "invoke_agent"
|
|
277
|
-
# 2. Has attribute gen_ai.operation.name set to INVOKE_AGENT_OPERATION_NAME
|
|
278
|
-
operation_name = attrs.get(GEN_AI_OPERATION_NAME_KEY)
|
|
279
|
-
if (
|
|
280
|
-
sp.name.startswith(INVOKE_AGENT_OPERATION_NAME)
|
|
281
|
-
and operation_name == INVOKE_AGENT_OPERATION_NAME
|
|
282
|
-
):
|
|
283
|
-
# Remove input messages attribute
|
|
284
|
-
attrs.pop(GEN_AI_INPUT_MESSAGES_KEY, None)
|
|
285
|
-
|
|
286
279
|
# events
|
|
287
280
|
events = []
|
|
288
281
|
for ev in sp.events:
|
|
@@ -19,22 +19,31 @@ class EnrichedReadableSpan(ReadableSpan):
|
|
|
19
19
|
the original span.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
def __init__(
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
span: ReadableSpan,
|
|
25
|
+
extra_attributes: dict,
|
|
26
|
+
excluded_attribute_keys: set[str] | None = None,
|
|
27
|
+
):
|
|
23
28
|
"""
|
|
24
29
|
Initialize the enriched span wrapper.
|
|
25
30
|
|
|
26
31
|
Args:
|
|
27
32
|
span: The original ReadableSpan to wrap.
|
|
28
33
|
extra_attributes: Additional attributes to merge with the original.
|
|
34
|
+
excluded_attribute_keys: Attribute keys to remove after merging.
|
|
29
35
|
"""
|
|
30
36
|
self._span = span
|
|
31
37
|
self._extra_attributes = extra_attributes
|
|
38
|
+
self._excluded_attribute_keys = excluded_attribute_keys or set()
|
|
32
39
|
|
|
33
40
|
@property
|
|
34
41
|
def attributes(self) -> types.Attributes:
|
|
35
42
|
"""Return merged attributes from original span and extra attributes."""
|
|
36
43
|
original = dict(self._span.attributes or {})
|
|
37
44
|
original.update(self._extra_attributes)
|
|
45
|
+
for key in self._excluded_attribute_keys:
|
|
46
|
+
original.pop(key, None)
|
|
38
47
|
return original
|
|
39
48
|
|
|
40
49
|
@property
|
|
@@ -10,6 +10,13 @@ from collections.abc import Callable
|
|
|
10
10
|
from opentelemetry.sdk.trace import ReadableSpan
|
|
11
11
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
12
12
|
|
|
13
|
+
from ..constants import (
|
|
14
|
+
GEN_AI_INPUT_MESSAGES_KEY,
|
|
15
|
+
GEN_AI_OPERATION_NAME_KEY,
|
|
16
|
+
INVOKE_AGENT_OPERATION_NAME,
|
|
17
|
+
)
|
|
18
|
+
from .enriched_span import EnrichedReadableSpan
|
|
19
|
+
|
|
13
20
|
logger = logging.getLogger(__name__)
|
|
14
21
|
|
|
15
22
|
# Single span enricher - only one platform instrumentor should be active at a time
|
|
@@ -65,6 +72,15 @@ def get_span_enricher() -> Callable[[ReadableSpan], ReadableSpan] | None:
|
|
|
65
72
|
class _EnrichingBatchSpanProcessor(BatchSpanProcessor):
|
|
66
73
|
"""BatchSpanProcessor that applies the registered enricher before batching."""
|
|
67
74
|
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
*args: object,
|
|
78
|
+
suppress_invoke_agent_input: bool = False,
|
|
79
|
+
**kwargs: object,
|
|
80
|
+
):
|
|
81
|
+
super().__init__(*args, **kwargs)
|
|
82
|
+
self._suppress_invoke_agent_input = suppress_invoke_agent_input
|
|
83
|
+
|
|
68
84
|
def on_end(self, span: ReadableSpan) -> None:
|
|
69
85
|
"""Apply the span enricher and pass to parent for batching.
|
|
70
86
|
|
|
@@ -83,4 +99,18 @@ class _EnrichingBatchSpanProcessor(BatchSpanProcessor):
|
|
|
83
99
|
enricher.__name__,
|
|
84
100
|
)
|
|
85
101
|
|
|
102
|
+
# Apply input message suppression for InvokeAgent spans
|
|
103
|
+
if self._suppress_invoke_agent_input:
|
|
104
|
+
attrs = enriched_span.attributes or {}
|
|
105
|
+
operation_name = attrs.get(GEN_AI_OPERATION_NAME_KEY)
|
|
106
|
+
if (
|
|
107
|
+
enriched_span.name.startswith(INVOKE_AGENT_OPERATION_NAME)
|
|
108
|
+
and operation_name == INVOKE_AGENT_OPERATION_NAME
|
|
109
|
+
):
|
|
110
|
+
enriched_span = EnrichedReadableSpan(
|
|
111
|
+
enriched_span,
|
|
112
|
+
extra_attributes={},
|
|
113
|
+
excluded_attribute_keys={GEN_AI_INPUT_MESSAGES_KEY},
|
|
114
|
+
)
|
|
115
|
+
|
|
86
116
|
super().on_end(enriched_span)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SpectraExporterOptions:
|
|
8
|
+
"""
|
|
9
|
+
Configuration for exporting traces to a Spectra Collector sidecar via OTLP.
|
|
10
|
+
|
|
11
|
+
Spectra Collector is deployed as a Kubernetes sidecar that accepts
|
|
12
|
+
standard OTLP telemetry on localhost. Defaults are tuned for this
|
|
13
|
+
deployment topology — most consumers should not need to override them.
|
|
14
|
+
|
|
15
|
+
Note: Batch processor fields (max_queue_size, scheduled_delay_ms, etc.)
|
|
16
|
+
are duplicated from Agent365ExporterOptions intentionally — these two
|
|
17
|
+
options classes have no shared base class per design decision C4.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_DEFAULT_GRPC_ENDPOINT = "http://localhost:4317"
|
|
21
|
+
_DEFAULT_HTTP_ENDPOINT = "http://localhost:4318"
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
endpoint: str | None = None,
|
|
26
|
+
protocol: Literal["grpc", "http"] = "grpc",
|
|
27
|
+
insecure: bool = True,
|
|
28
|
+
max_queue_size: int = 2048,
|
|
29
|
+
scheduled_delay_ms: int = 5000,
|
|
30
|
+
exporter_timeout_ms: int = 30000,
|
|
31
|
+
max_export_batch_size: int = 512,
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Args:
|
|
35
|
+
endpoint: Spectra sidecar OTLP endpoint. Defaults to
|
|
36
|
+
http://localhost:4317 for gRPC or http://localhost:4318 for HTTP.
|
|
37
|
+
protocol: OTLP protocol — "grpc" or "http". Default: grpc.
|
|
38
|
+
insecure: Use insecure (no TLS) connection. Default: True (localhost sidecar).
|
|
39
|
+
max_queue_size: Batch processor queue size. Default: 2048.
|
|
40
|
+
scheduled_delay_ms: Export interval in milliseconds. Default: 5000.
|
|
41
|
+
exporter_timeout_ms: Export timeout in milliseconds. Default: 30000.
|
|
42
|
+
max_export_batch_size: Max spans per export batch. Default: 512.
|
|
43
|
+
"""
|
|
44
|
+
if protocol not in ("grpc", "http"):
|
|
45
|
+
raise ValueError(f"protocol must be 'grpc' or 'http', got '{protocol}'")
|
|
46
|
+
if endpoint is None:
|
|
47
|
+
endpoint = (
|
|
48
|
+
self._DEFAULT_GRPC_ENDPOINT if protocol == "grpc" else self._DEFAULT_HTTP_ENDPOINT
|
|
49
|
+
)
|
|
50
|
+
self.endpoint = endpoint
|
|
51
|
+
self.protocol = protocol
|
|
52
|
+
self.insecure = insecure
|
|
53
|
+
self.max_queue_size = max_queue_size
|
|
54
|
+
self.scheduled_delay_ms = scheduled_delay_ms
|
|
55
|
+
self.exporter_timeout_ms = exporter_timeout_ms
|
|
56
|
+
self.max_export_batch_size = max_export_batch_size
|
|
@@ -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))
|
|
@@ -24,6 +24,7 @@ microsoft_agents_a365/observability/core/exporters/agent365_exporter.py
|
|
|
24
24
|
microsoft_agents_a365/observability/core/exporters/agent365_exporter_options.py
|
|
25
25
|
microsoft_agents_a365/observability/core/exporters/enriched_span.py
|
|
26
26
|
microsoft_agents_a365/observability/core/exporters/enriching_span_processor.py
|
|
27
|
+
microsoft_agents_a365/observability/core/exporters/spectra_exporter_options.py
|
|
27
28
|
microsoft_agents_a365/observability/core/exporters/utils.py
|
|
28
29
|
microsoft_agents_a365/observability/core/middleware/__init__.py
|
|
29
30
|
microsoft_agents_a365/observability/core/middleware/baggage_builder.py
|
|
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
|