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.
Files changed (47) hide show
  1. {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/PKG-INFO +1 -1
  2. {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
  3. {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
  4. {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
  5. {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
  6. {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
  7. {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
  8. microsoft_agents_a365_observability_core-0.2.1.dev42/microsoft_agents_a365/observability/core/exporters/spectra_exporter_options.py +56 -0
  9. {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
  10. {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
  11. {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
  12. {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
  13. {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
  14. {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/README.md +0 -0
  15. {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
  16. {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
  17. {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
  18. {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
  19. {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
  20. {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
  21. {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
  22. {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
  23. {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
  24. {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
  25. {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
  26. {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
  27. {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
  28. {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
  29. {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
  30. {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
  31. {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
  32. {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
  33. {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
  34. {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
  35. {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
  36. {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
  37. {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
  38. {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
  39. {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
  40. {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
  41. {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
  42. {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
  43. {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
  44. {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
  45. {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/pyproject.toml +0 -0
  46. {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/setup.cfg +0 -0
  47. {microsoft_agents_a365_observability_core-0.2.1.dev35 → microsoft_agents_a365_observability_core-0.2.1.dev42}/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.dev35
3
+ Version: 0.2.1.dev42
4
4
  Summary: Telemetry, tracing, and monitoring components for AI agents
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -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: Optional[Agent365ExporterOptions] = None,
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 instance for configuring the exporter.
76
- If provided, exporter_options takes precedence. If exporter_options is None, the token_resolver and cluster_category parameters are used as fallback/legacy support to construct a default Agent365ExporterOptions instance.
77
- :param suppress_invoke_agent_input: If True, suppress input messages for spans that are children of InvokeAgent spans.
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: Optional[Agent365ExporterOptions] = None,
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
- if is_agent365_exporter_enabled() and exporter_options.token_resolver is not None:
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. Falling back to console exporter."
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(exporter, **batch_processor_kwargs)
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: Optional[Agent365ExporterOptions] = None,
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 instance for configuring the exporter.
265
- If provided, exporter_options takes precedence. If exporter_options is None, the token_resolver and cluster_category parameters are used as fallback/legacy support to construct a default Agent365ExporterOptions instance.
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
- # Debug: Log number of groups and total span count
83
+ # Log number of groups and total span count
90
84
  total_spans = sum(len(activities) for activities in groups.values())
91
- logger.info(
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
- # Debug: Log endpoint being used
109
- logger.info(
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.info(f"Token resolved successfully for agent {agent_id}")
119
+ logger.debug(f"Token resolved successfully for agent {agent_id}")
120
120
  else:
121
- logger.info(f"No token returned for agent {agent_id}")
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.info(
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
- 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))
198
204
  continue
199
205
  # Final attempt failed
200
206
  logger.error(
201
- 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. "
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
- time.sleep(0.2 * (attempt + 1))
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__(self, span: ReadableSpan, extra_attributes: dict):
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 baggage, context, trace
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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-observability-core
3
- Version: 0.2.1.dev35
3
+ Version: 0.2.1.dev42
4
4
  Summary: Telemetry, tracing, and monitoring components for AI agents
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -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