microsoft-agents-a365-observability-core 0.2.1.dev7__py3-none-any.whl → 0.2.1.dev10__py3-none-any.whl

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.
@@ -12,6 +12,12 @@ from .config import (
12
12
  )
13
13
  from .execute_tool_scope import ExecuteToolScope
14
14
  from .execution_type import ExecutionType
15
+ from .exporters.enriched_span import EnrichedReadableSpan
16
+ from .exporters.enriching_span_processor import (
17
+ get_span_enricher,
18
+ register_span_enricher,
19
+ unregister_span_enricher,
20
+ )
15
21
  from .inference_call_details import InferenceCallDetails
16
22
  from .inference_operation_type import InferenceOperationType
17
23
  from .inference_scope import InferenceScope
@@ -32,6 +38,11 @@ __all__ = [
32
38
  "is_configured",
33
39
  "get_tracer",
34
40
  "get_tracer_provider",
41
+ # Span enrichment
42
+ "register_span_enricher",
43
+ "unregister_span_enricher",
44
+ "get_span_enricher",
45
+ "EnrichedReadableSpan",
35
46
  # Span processor
36
47
  "SpanProcessor",
37
48
  # Base scope class
@@ -9,10 +9,13 @@ from typing import Any, Optional
9
9
  from opentelemetry import trace
10
10
  from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_NAMESPACE, Resource
11
11
  from opentelemetry.sdk.trace import TracerProvider
12
- from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
12
+ from opentelemetry.sdk.trace.export import ConsoleSpanExporter
13
13
 
14
14
  from .exporters.agent365_exporter import _Agent365Exporter
15
15
  from .exporters.agent365_exporter_options import Agent365ExporterOptions
16
+ from .exporters.enriching_span_processor import (
17
+ _EnrichingBatchSpanProcessor,
18
+ )
16
19
  from .exporters.utils import is_agent365_exporter_enabled
17
20
  from .trace_processor.span_processor import SpanProcessor
18
21
 
@@ -166,8 +169,9 @@ class TelemetryManager:
166
169
 
167
170
  # Add span processors
168
171
 
169
- # Create BatchSpanProcessor with optimized settings
170
- batch_processor = BatchSpanProcessor(exporter, **batch_processor_kwargs)
172
+ # Create _EnrichingBatchSpanProcessor with optimized settings
173
+ # This allows extensions to enrich spans before export
174
+ batch_processor = _EnrichingBatchSpanProcessor(exporter, **batch_processor_kwargs)
171
175
  agent_processor = SpanProcessor()
172
176
 
173
177
  tracer_provider.add_span_processor(batch_processor)
@@ -0,0 +1,160 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """Enriched ReadableSpan wrapper for adding attributes to immutable spans."""
5
+
6
+ import json
7
+ from typing import Any
8
+
9
+ from opentelemetry.sdk.trace import ReadableSpan
10
+ from opentelemetry.util import types
11
+
12
+
13
+ class EnrichedReadableSpan(ReadableSpan):
14
+ """
15
+ Wrapper to add attributes to an immutable ReadableSpan.
16
+
17
+ Since ReadableSpan is immutable after a span ends, this wrapper allows
18
+ extensions to add additional attributes before export without modifying
19
+ the original span.
20
+ """
21
+
22
+ def __init__(self, span: ReadableSpan, extra_attributes: dict):
23
+ """
24
+ Initialize the enriched span wrapper.
25
+
26
+ Args:
27
+ span: The original ReadableSpan to wrap.
28
+ extra_attributes: Additional attributes to merge with the original.
29
+ """
30
+ self._span = span
31
+ self._extra_attributes = extra_attributes
32
+
33
+ @property
34
+ def attributes(self) -> types.Attributes:
35
+ """Return merged attributes from original span and extra attributes."""
36
+ original = dict(self._span.attributes or {})
37
+ original.update(self._extra_attributes)
38
+ return original
39
+
40
+ @property
41
+ def name(self):
42
+ """Return the span name."""
43
+ return self._span.name
44
+
45
+ @property
46
+ def context(self):
47
+ """Return the span context."""
48
+ return self._span.context
49
+
50
+ @property
51
+ def parent(self):
52
+ """Return the parent span context."""
53
+ return self._span.parent
54
+
55
+ @property
56
+ def start_time(self):
57
+ """Return the span start time."""
58
+ return self._span.start_time
59
+
60
+ @property
61
+ def end_time(self):
62
+ """Return the span end time."""
63
+ return self._span.end_time
64
+
65
+ @property
66
+ def status(self):
67
+ """Return the span status."""
68
+ return self._span.status
69
+
70
+ @property
71
+ def kind(self):
72
+ """Return the span kind."""
73
+ return self._span.kind
74
+
75
+ @property
76
+ def events(self):
77
+ """Return the span events."""
78
+ return self._span.events
79
+
80
+ @property
81
+ def links(self):
82
+ """Return the span links."""
83
+ return self._span.links
84
+
85
+ @property
86
+ def resource(self):
87
+ """Return the span resource."""
88
+ return self._span.resource
89
+
90
+ @property
91
+ def instrumentation_scope(self):
92
+ """Return the instrumentation scope."""
93
+ return self._span.instrumentation_scope
94
+
95
+ def to_json(self, indent: int | None = 4) -> str:
96
+ """
97
+ Convert span to JSON string with enriched attributes.
98
+
99
+ Args:
100
+ indent: JSON indentation level.
101
+
102
+ Returns:
103
+ JSON string representation of the span.
104
+ """
105
+ # Build the JSON dict manually to include enriched attributes
106
+ return json.dumps(
107
+ {
108
+ "name": self.name,
109
+ "context": {
110
+ "trace_id": f"0x{self.context.trace_id:032x}",
111
+ "span_id": f"0x{self.context.span_id:016x}",
112
+ "trace_state": str(self.context.trace_state),
113
+ }
114
+ if self.context
115
+ else None,
116
+ "kind": str(self.kind),
117
+ "parent_id": f"0x{self.parent.span_id:016x}" if self.parent else None,
118
+ "start_time": self._format_time(self.start_time),
119
+ "end_time": self._format_time(self.end_time),
120
+ "status": {
121
+ "status_code": str(self.status.status_code),
122
+ "description": self.status.description,
123
+ }
124
+ if self.status
125
+ else None,
126
+ "attributes": dict(self.attributes) if self.attributes else None,
127
+ "events": [self._format_event(e) for e in self.events] if self.events else None,
128
+ "links": [self._format_link(lnk) for lnk in self.links] if self.links else None,
129
+ "resource": dict(self.resource.attributes) if self.resource else None,
130
+ },
131
+ indent=indent,
132
+ )
133
+
134
+ def _format_time(self, time_ns: int | None) -> str | None:
135
+ """Format nanosecond timestamp to ISO string."""
136
+ if time_ns is None:
137
+ return None
138
+ from datetime import datetime, timezone
139
+
140
+ return datetime.fromtimestamp(time_ns / 1e9, tz=timezone.utc).isoformat()
141
+
142
+ def _format_event(self, event: Any) -> dict:
143
+ """Format a span event."""
144
+ return {
145
+ "name": event.name,
146
+ "timestamp": self._format_time(event.timestamp),
147
+ "attributes": dict(event.attributes) if event.attributes else None,
148
+ }
149
+
150
+ def _format_link(self, link: Any) -> dict:
151
+ """Format a span link."""
152
+ return {
153
+ "context": {
154
+ "trace_id": f"0x{link.context.trace_id:032x}",
155
+ "span_id": f"0x{link.context.span_id:016x}",
156
+ }
157
+ if link.context
158
+ else None,
159
+ "attributes": dict(link.attributes) if link.attributes else None,
160
+ }
@@ -0,0 +1,86 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """Span enrichment support for the Agent365 exporter pipeline."""
5
+
6
+ import logging
7
+ import threading
8
+ from collections.abc import Callable
9
+
10
+ from opentelemetry.sdk.trace import ReadableSpan
11
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Single span enricher - only one platform instrumentor should be active at a time
16
+ _span_enricher: Callable[[ReadableSpan], ReadableSpan] | None = None
17
+ _enricher_lock = threading.Lock()
18
+
19
+
20
+ def register_span_enricher(enricher: Callable[[ReadableSpan], ReadableSpan]) -> None:
21
+ """Register the span enricher for the active platform instrumentor.
22
+
23
+ Only one enricher can be registered at a time since auto-instrumentation
24
+ is platform-specific (Semantic Kernel, LangChain, or OpenAI Agents).
25
+
26
+ Args:
27
+ enricher: Function that takes a ReadableSpan and returns an enriched span.
28
+
29
+ Raises:
30
+ RuntimeError: If an enricher is already registered.
31
+ """
32
+ global _span_enricher
33
+ with _enricher_lock:
34
+ if _span_enricher is not None:
35
+ raise RuntimeError(
36
+ "A span enricher is already registered. "
37
+ "Only one platform instrumentor can be active at a time."
38
+ )
39
+ _span_enricher = enricher
40
+ logger.debug("Span enricher registered: %s", enricher.__name__)
41
+
42
+
43
+ def unregister_span_enricher() -> None:
44
+ """Unregister the current span enricher.
45
+
46
+ Called during uninstrumentation to clean up.
47
+ """
48
+ global _span_enricher
49
+ with _enricher_lock:
50
+ if _span_enricher is not None:
51
+ logger.debug("Span enricher unregistered: %s", _span_enricher.__name__)
52
+ _span_enricher = None
53
+
54
+
55
+ def get_span_enricher() -> Callable[[ReadableSpan], ReadableSpan] | None:
56
+ """Get the currently registered span enricher.
57
+
58
+ Returns:
59
+ The registered enricher function, or None if no enricher is registered.
60
+ """
61
+ with _enricher_lock:
62
+ return _span_enricher
63
+
64
+
65
+ class _EnrichingBatchSpanProcessor(BatchSpanProcessor):
66
+ """BatchSpanProcessor that applies the registered enricher before batching."""
67
+
68
+ def on_end(self, span: ReadableSpan) -> None:
69
+ """Apply the span enricher and pass to parent for batching.
70
+
71
+ Args:
72
+ span: The span that has ended.
73
+ """
74
+ enriched_span = span
75
+
76
+ enricher = get_span_enricher()
77
+ if enricher is not None:
78
+ try:
79
+ enriched_span = enricher(span)
80
+ except Exception:
81
+ logger.exception(
82
+ "Span enricher %s raised an exception, using original span",
83
+ enricher.__name__,
84
+ )
85
+
86
+ super().on_end(enriched_span)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-observability-core
3
- Version: 0.2.1.dev7
3
+ Version: 0.2.1.dev10
4
4
  Summary: Telemetry, tracing, and monitoring components for AI agents
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
- microsoft_agents_a365/observability/core/__init__.py,sha256=r613vGLepo9ttTRmq3_EQz3hfDd4Ukp3v8AVS-6Uf_o,1762
1
+ microsoft_agents_a365/observability/core/__init__.py,sha256=1wuG2_VL_XF0aNZ2EPF3M_3VGZe70LP6KAhS5UL73wQ,2090
2
2
  microsoft_agents_a365/observability/core/agent_details.py,sha256=7x4tqcocig1hz4T6eCRZy7fSF2gOCpM0pvE6Q3Txww0,1210
3
- microsoft_agents_a365/observability/core/config.py,sha256=YhnQaJr-SVNJLGM_HvYvaAfwXn-h9KinbNNZeXG9w90,11751
3
+ microsoft_agents_a365/observability/core/config.py,sha256=p8p9Chyol6Ar-qM6MfHWl6mTw4lNupSagjArHclVg3M,11900
4
4
  microsoft_agents_a365/observability/core/constants.py,sha256=suvJc4JgB9o3eeDVyiJDk0kjFVkjeDrl6u43XsvPWBc,4747
5
5
  microsoft_agents_a365/observability/core/execute_tool_scope.py,sha256=XgramOBLhpilR5VjFsigdDCqYkDkG0D6CIrwN5xgeJw,3654
6
6
  microsoft_agents_a365/observability/core/execution_type.py,sha256=qnNGmo9-Tlb3PBTK3pen2QIAm2hlQiKl8vRuMPWHtk8,328
@@ -19,6 +19,8 @@ microsoft_agents_a365/observability/core/utils.py,sha256=fnZBCEsD-1FlxpJ5hcbwovr
19
19
  microsoft_agents_a365/observability/core/exporters/__init__.py,sha256=UEaMJYkbTViVAUVQTFSf1BUvni1pNTcDwriKzOzvGFU,296
20
20
  microsoft_agents_a365/observability/core/exporters/agent365_exporter.py,sha256=Q8ylOrgBF2gNB_KAn8QdtJu8E3aiC6kUUomCgNLqgb4,13490
21
21
  microsoft_agents_a365/observability/core/exporters/agent365_exporter_options.py,sha256=f-p11ZQfxtDIiK-kVPDfebBgT83onxdE29IQryYoAPk,1648
22
+ microsoft_agents_a365/observability/core/exporters/enriched_span.py,sha256=PcXw4M-he-CuCw5OZT4ZbIbc1Uvskvp_N3Dz3qiNnPc,5056
23
+ microsoft_agents_a365/observability/core/exporters/enriching_span_processor.py,sha256=9JXCMRjrKTc2VJ_rmSX8u5_DopBN_AR4WwyhFL9H4Ic,2781
22
24
  microsoft_agents_a365/observability/core/exporters/utils.py,sha256=tICuOuWfgUKL1dHoGuyG5_xNOrREPTXFE-RJZehpVT4,6989
23
25
  microsoft_agents_a365/observability/core/middleware/__init__.py,sha256=KNH2QjJBIGhn1xf_r3HdOVYUw9ngKEcy0HV9eHuTaqk,202
24
26
  microsoft_agents_a365/observability/core/middleware/baggage_builder.py,sha256=KaTb8n0D8dbjS-qph3o160VUEn4kYy8YdaFvGMd5Vck,10505
@@ -29,7 +31,7 @@ microsoft_agents_a365/observability/core/models/operation_source.py,sha256=HJp-S
29
31
  microsoft_agents_a365/observability/core/trace_processor/__init__.py,sha256=0QDkZgvgGekJVcZXjubSqipYhCGMZaAAxLSanyR76pg,219
30
32
  microsoft_agents_a365/observability/core/trace_processor/span_processor.py,sha256=O_DLuLR-6sV9VmkYfMzUF3s6n1OLrtK5qkD6csen73g,2980
31
33
  microsoft_agents_a365/observability/core/trace_processor/util.py,sha256=h5oOFAGIY6x9WtgfErPetblSkWZsQZjOgbwoR_tL-7k,2364
32
- microsoft_agents_a365_observability_core-0.2.1.dev7.dist-info/METADATA,sha256=5hQwVbA2sbJRdEZFebNaI_vMXRlurtx4XsOBUi1Jidg,3871
33
- microsoft_agents_a365_observability_core-0.2.1.dev7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
34
- microsoft_agents_a365_observability_core-0.2.1.dev7.dist-info/top_level.txt,sha256=G3c2_4sy5_EM_BWO67SbK2tKj4G8XFn-QXRbh8g9Lgk,22
35
- microsoft_agents_a365_observability_core-0.2.1.dev7.dist-info/RECORD,,
34
+ microsoft_agents_a365_observability_core-0.2.1.dev10.dist-info/METADATA,sha256=5za0Ke8X9hZuZR1NpRcM9JWydFX6MCUqR--k_n_-yU4,3872
35
+ microsoft_agents_a365_observability_core-0.2.1.dev10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
36
+ microsoft_agents_a365_observability_core-0.2.1.dev10.dist-info/top_level.txt,sha256=G3c2_4sy5_EM_BWO67SbK2tKj4G8XFn-QXRbh8g9Lgk,22
37
+ microsoft_agents_a365_observability_core-0.2.1.dev10.dist-info/RECORD,,