Skip to content

Commit 6028e07

Browse files
committed
fix(maf): delegate metrics and preserve context
1 parent be4f7d3 commit 6028e07

17 files changed

Lines changed: 1752 additions & 559 deletions

File tree

instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Changed
11+
12+
- Delegate GenAI metric recording to the shared ``opentelemetry-util-genai``
13+
existing-span lifecycle and remove the process-cumulative ObservableGauges.
14+
- Stop using ``ARMS_MAF_SLOW_THRESHOLD_MS`` because slow-call aggregation is no
15+
longer owned here. Its configuration helper and legacy
16+
``MAFSemanticProcessor`` constructor arguments remain as deprecated no-ops.
17+
- ``ARMS_MAF_METRICS_ENABLED`` now controls only extension metrics recorded
18+
through ``opentelemetry-util-genai``; MAF's native standard metrics are
19+
unaffected.
20+
1021
### Fixed
1122

1223
- Avoid enriching non-MAF spans that use overlapping GenAI operation names.
1324
- Preserve parent-child context for legacy Microsoft Agent Framework streaming
1425
agent spans.
26+
- Record streaming TTFT only after ``ResponseStream`` returns its first update,
27+
and keep finalization safe across concurrent threads and ``fork()``.
28+
- Emit the standard failed-call metric through ``opentelemetry-util-genai``
29+
when MAF's native LLM error path does not reach its metric capture, and route
30+
MAF's tracer and meter accessors to explicitly supplied providers.
31+
- Adopt streaming spans created directly by MAF 1.0 through 1.2.1 so shared
32+
finalization still runs before their native ``span.end()``.
1533

1634
## Version 0.7.0 (2026-07-03)
1735

instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/README.rst

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,25 @@ through ``opentelemetry-util-genai`` before spans are ended:
1717
token usage, finish reasons, and streaming TTFT where MAF exposes enough
1818
data.
1919
* ``MAFSemanticProcessor`` remains as a compatibility layer for MAF workflow,
20-
MCP, private-prefix attribute normalization, and the in-process ARMS gauges.
21-
Successful spans keep the OpenTelemetry default ``UNSET`` status; failed
22-
spans keep MAF's ``ERROR`` status.
20+
MCP, and private-prefix attribute normalization. Metrics are recorded through
21+
the shared ``opentelemetry-util-genai`` existing-span lifecycle, without
22+
creating a second GenAI span. Successful spans keep the OpenTelemetry default
23+
``UNSET`` status; failed spans keep MAF's ``ERROR`` status.
2324
* ``react_step_patch`` (opt-in via ``ARMS_MAF_REACT_STEP_ENABLED=true``)
2425
wraps ``FunctionInvocationLayer.get_response`` so that each LLM round-trip
2526
inside the ReAct loop emits one ``react step`` span via
2627
``ExtendedTelemetryHandler.react_step()`` from ``opentelemetry-util-genai``.
2728

29+
When explicit tracer or meter providers are passed to the instrumentor, MAF's
30+
native provider accessors are routed to them. Instrument before constructing
31+
MAF clients so their native metric instruments bind to the requested provider.
32+
33+
Compatibility note: MAF 1.0 through 1.2.1 creates streaming Chat spans without
34+
a per-pull context hook. The processor still installs pre-end shared
35+
finalization for those spans, but pull-derived TTFT and Chat-child parenting are
36+
available only with MAF 1.2.2 or newer. This is an upstream lifecycle boundary;
37+
non-streaming telemetry remains supported on the older versions.
38+
2839
Truncation / PII helpers are reused from ``opentelemetry.util.genai.utils``
2940
(``gen_ai_json_dumps``), aligned with
3041
the OpenAI Agents v2 GenAI instrumentation.
@@ -47,6 +58,12 @@ instrumentation package and framework separately:
4758
pip install loongsuite-instrumentation-microsoft-agent-framework
4859
pip install agent-framework-core
4960
61+
Published LoongSuite wheels depend on the matching
62+
``loongsuite-otel-util-genai`` release. For a source-checkout development
63+
install, build or install ``util/opentelemetry-util-genai`` from the same
64+
checkout first so the shared existing-span API resolves from that checkout
65+
rather than another distribution using the same import namespace.
66+
5067
Configuration
5168
=============
5269

@@ -56,6 +73,5 @@ Env Default Description
5673
``ARMS_MAF_INSTRUMENTATION_ENABLED`` ``true`` Master switch; ``false`` disables instrumentation.
5774
``ARMS_MAF_SENSITIVE_DATA_ENABLED`` ``false`` Capture inputs/outputs (linked to MAF's sensitive-data option).
5875
``ARMS_MAF_REACT_STEP_ENABLED`` ``false`` Emit ``react step`` spans for non-streaming ReAct tool loops.
59-
``ARMS_MAF_METRICS_ENABLED`` ``true`` Aggregate ARMS GenAI gauges in-process.
60-
``ARMS_MAF_SLOW_THRESHOLD_MS`` ``1000`` Slow-call threshold in ms.
76+
``ARMS_MAF_METRICS_ENABLED`` ``true`` Record extension metrics through ``opentelemetry-util-genai``; native MAF metrics are unaffected.
6177
====================================== ========== ==============================================================

instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
with ``force=True`` so a sticky user-disable does not block us), bridges MAF's
1919
native span helpers through ``opentelemetry-util-genai`` finish helpers, and
2020
registers :class:`~.span_processor.MAFSemanticProcessor` for workflow/MCP
21-
normalization plus metrics aggregation.
21+
normalization. GenAI metrics are delegated to ``opentelemetry-util-genai``.
2222
2323
The optional ReAct step patch (``ARMS_MAF_REACT_STEP_ENABLED=true``) wraps the
2424
``FunctionInvocationLayer.get_response`` ReAct loop with
@@ -42,7 +42,6 @@
4242
from opentelemetry.trace import get_tracer_provider
4343

4444
from .config import (
45-
get_slow_threshold_ms,
4645
is_instrumentation_enabled,
4746
is_metrics_enabled,
4847
is_react_step_enabled,
@@ -116,20 +115,32 @@ def _instrument(self, **kwargs: Any) -> None:
116115
# invocation finish helpers. This keeps MAF's span lifetime and
117116
# streaming cleanup behavior, but writes AGENT/LLM/TOOL semantic
118117
# attributes before span.end() creates the exporter snapshot.
119-
apply_util_genai_bridge()
118+
apply_util_genai_bridge(
119+
tracer_provider=tracer_provider,
120+
meter_provider=meter_provider,
121+
metrics_enabled=is_metrics_enabled(default=True),
122+
)
120123

121124
# 3) Register the semantic SpanProcessor. MAF uses the standard OTel
122125
# TracerProvider (it does not have its own multi-processor), so
123126
# ``add_span_processor`` is the right hook.
124127
processor = MAFSemanticProcessor(
125-
meter_provider=meter_provider,
126-
slow_threshold_ms=get_slow_threshold_ms(),
127-
metrics_enabled=is_metrics_enabled(default=True),
128128
capture_sensitive_data=sensitive,
129129
)
130130
try:
131131
tracer_provider.add_span_processor(processor)
132132
except Exception as exc:
133+
# BaseInstrumentor does not mark the instrumentor active when
134+
# _instrument raises, so a later uninstrument cannot clean up a
135+
# bridge installed above. Roll back this partial transaction now.
136+
revert_util_genai_bridge()
137+
try:
138+
_remove_span_processor(tracer_provider, processor)
139+
processor.shutdown()
140+
except Exception as cleanup_exc: # pragma: no cover - defensive
141+
logger.debug(
142+
"processor registration cleanup failed: %s", cleanup_exc
143+
)
133144
logger.warning("add_span_processor failed: %s", exc)
134145
raise
135146

instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def is_metrics_enabled(default: bool = True) -> bool:
6161

6262

6363
def get_slow_threshold_ms(default: int = 1000) -> int:
64+
"""Return the deprecated slow threshold for API compatibility only."""
6465
raw = os.getenv(ENV_SLOW_THRESHOLD_MS)
6566
if raw is None:
6667
return default

0 commit comments

Comments
 (0)