From 1ba43a45ae6469f29fa0f33ba14520a0eab051e9 Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 10:12:11 +0530 Subject: [PATCH 01/16] fix tracing for operation id to flow in exception table. --- .../azure/ai/agentserver/core/_tracing.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index f9ab0ff60750..1bd7cdabd51e 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -294,14 +294,23 @@ async def __call__(self, scope: Any, receive: Any, send: Any) -> None: "x_request_id", x_request_id, context=ctx, ) - token = _otel_context.attach(ctx) - try: - await self.app(scope, receive, send) - finally: - try: - _otel_context.detach(token) - except ValueError: - pass + # Create a NonRecordingSpan with the extracted trace context so that + # get_current_span() returns a span carrying the correct trace_id. + # Without this, the OTel LogRecord processor sees no active span and + # sets trace_id=0, causing zeroed operation_Id in Application Insights + # for logs emitted by Hypercorn's access logger. + span = trace.get_current_span(ctx) + span_ctx = span.get_span_context() + if span_ctx and span_ctx.trace_id: + non_recording = trace.NonRecordingSpan(span_ctx) + ctx = trace.set_span_in_context(non_recording, ctx) + + # Attach the context and do NOT detach in finally. Hypercorn emits + # its access log after the ASGI app returns but within the same async + # task. Detaching here would clear the trace context before Hypercorn + # logs. The context is cleaned up when the async task ends. + _otel_context.attach(ctx) + await self.app(scope, receive, send) def end_span(span: Any, exc: Optional[BaseException] = None) -> None: From 13ecc29eacc8d69ce14e49367b5f4e0cf978f44e Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 13:31:15 +0530 Subject: [PATCH 02/16] Fix OTel context lifecycle in tracing middleware Defer context detach by one event-loop turn so post-response access logging can still observe request trace context, while still restoring prior context to prevent leakage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/core/_tracing.py | 17 +++-- .../tests/test_tracing.py | 68 ++++++++++++++++++- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index 1bd7cdabd51e..40d71c709ab4 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -33,6 +33,7 @@ OpenTelemetry is a required dependency — these functions always create real spans. Azure Monitor export is optional (auto-configured by the distro). """ +import asyncio import logging import os from collections.abc import AsyncIterable, AsyncIterator # pylint: disable=import-error @@ -301,16 +302,18 @@ async def __call__(self, scope: Any, receive: Any, send: Any) -> None: # for logs emitted by Hypercorn's access logger. span = trace.get_current_span(ctx) span_ctx = span.get_span_context() - if span_ctx and span_ctx.trace_id: + if span_ctx and span_ctx.is_valid: non_recording = trace.NonRecordingSpan(span_ctx) ctx = trace.set_span_in_context(non_recording, ctx) - # Attach the context and do NOT detach in finally. Hypercorn emits - # its access log after the ASGI app returns but within the same async - # task. Detaching here would clear the trace context before Hypercorn - # logs. The context is cleaned up when the async task ends. - _otel_context.attach(ctx) - await self.app(scope, receive, send) + # Attach request context for app execution and defer detach by one + # event-loop turn so server-side access logging that runs immediately + # after app return can still observe the request trace context. + token = _otel_context.attach(ctx) + try: + await self.app(scope, receive, send) + finally: + asyncio.get_running_loop().call_soon(detach_context, token) def end_span(span: Any, exc: Optional[BaseException] = None) -> None: diff --git a/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py index 600b587afab6..c9dc4fcf38be 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py @@ -2,6 +2,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # --------------------------------------------------------- """Tests for tracing configuration — not invocation spans (those live in the invocations package).""" +import asyncio import os from unittest import mock @@ -16,7 +17,7 @@ resolve_agent_version, resolve_appinsights_connection_string, ) -from azure.ai.agentserver.core._tracing import _FoundryEnrichmentSpanProcessor +from azure.ai.agentserver.core._tracing import TraceContextMiddleware, _FoundryEnrichmentSpanProcessor class _CollectorExporter(SpanExporter): @@ -431,4 +432,69 @@ def test_agent_version_default_empty(self) -> None: assert resolve_agent_version() == "" +class TestTraceContextMiddleware: + """Trace context middleware lifecycle behavior.""" + + def test_detach_is_deferred_until_after_app_returns(self) -> None: + events = [] + + async def app(scope, receive, send): + events.append("app-called") + + async def run_test(): + middleware = TraceContextMiddleware(app) + scope = {"type": "http", "headers": []} + + async def receive(): + return {} + + async def send(_message): + return None + + with mock.patch("azure.ai.agentserver.core._tracing._otel_context.attach", return_value="token") as attach_mock, \ + mock.patch("azure.ai.agentserver.core._tracing._otel_context.detach") as detach_mock: + await middleware(scope, receive, send) + assert events == ["app-called"] + attach_mock.assert_called_once() + detach_mock.assert_not_called() + + await asyncio.sleep(0) + detach_mock.assert_called_once_with("token") + + asyncio.run(run_test()) + + def test_detach_still_happens_when_app_raises(self) -> None: + class _AppError(RuntimeError): + pass + + async def app(scope, receive, send): + raise _AppError("boom") + + async def run_test(): + middleware = TraceContextMiddleware(app) + scope = {"type": "http", "headers": []} + + async def receive(): + return {} + + async def send(_message): + return None + + with mock.patch("azure.ai.agentserver.core._tracing._otel_context.attach", return_value="token") as attach_mock, \ + mock.patch("azure.ai.agentserver.core._tracing._otel_context.detach") as detach_mock: + try: + await middleware(scope, receive, send) + except _AppError: + pass + else: + raise AssertionError("expected middleware to propagate app exception") + + attach_mock.assert_called_once() + detach_mock.assert_not_called() + + await asyncio.sleep(0) + detach_mock.assert_called_once_with("token") + + asyncio.run(run_test()) + From 66ae7a2cb0d25322804445387aa2b5e0a2417442 Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 13:46:51 +0530 Subject: [PATCH 03/16] Add test for safe deferred OTel detach Validate detach_context swallows ValueError so call_soon(detach_context, token) cannot surface non-current-token detach errors as unhandled callback failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/test_tracing.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py index c9dc4fcf38be..2b561727cb53 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py @@ -17,7 +17,11 @@ resolve_agent_version, resolve_appinsights_connection_string, ) -from azure.ai.agentserver.core._tracing import TraceContextMiddleware, _FoundryEnrichmentSpanProcessor +from azure.ai.agentserver.core._tracing import ( + TraceContextMiddleware, + _FoundryEnrichmentSpanProcessor, + detach_context, +) class _CollectorExporter(SpanExporter): @@ -498,3 +502,14 @@ async def send(_message): asyncio.run(run_test()) +class TestDetachContext: + """Tests for detach context helper behavior.""" + + def test_detach_context_ignores_value_error(self) -> None: + with mock.patch( + "azure.ai.agentserver.core._tracing._otel_context.detach", + side_effect=ValueError("non-current token"), + ) as detach_mock: + detach_context("token") + detach_mock.assert_called_once_with("token") + From 01c74fec8d00fdbcbfd62bd99768b678be0baafe Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 13:48:14 +0530 Subject: [PATCH 04/16] Revert "Add test for safe deferred OTel detach" This reverts commit 66ae7a2cb0d25322804445387aa2b5e0a2417442. --- .../tests/test_tracing.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py index 2b561727cb53..c9dc4fcf38be 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py @@ -17,11 +17,7 @@ resolve_agent_version, resolve_appinsights_connection_string, ) -from azure.ai.agentserver.core._tracing import ( - TraceContextMiddleware, - _FoundryEnrichmentSpanProcessor, - detach_context, -) +from azure.ai.agentserver.core._tracing import TraceContextMiddleware, _FoundryEnrichmentSpanProcessor class _CollectorExporter(SpanExporter): @@ -502,14 +498,3 @@ async def send(_message): asyncio.run(run_test()) -class TestDetachContext: - """Tests for detach context helper behavior.""" - - def test_detach_context_ignores_value_error(self) -> None: - with mock.patch( - "azure.ai.agentserver.core._tracing._otel_context.detach", - side_effect=ValueError("non-current token"), - ) as detach_mock: - detach_context("token") - detach_mock.assert_called_once_with("token") - From 52a9c9b33b69c1d128c67c4bf153924b343b779e Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 13:51:31 +0530 Subject: [PATCH 05/16] Reset test_tracing.py to main Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/test_tracing.py | 68 +------------------ 1 file changed, 1 insertion(+), 67 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py index c9dc4fcf38be..600b587afab6 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py @@ -2,7 +2,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # --------------------------------------------------------- """Tests for tracing configuration — not invocation spans (those live in the invocations package).""" -import asyncio import os from unittest import mock @@ -17,7 +16,7 @@ resolve_agent_version, resolve_appinsights_connection_string, ) -from azure.ai.agentserver.core._tracing import TraceContextMiddleware, _FoundryEnrichmentSpanProcessor +from azure.ai.agentserver.core._tracing import _FoundryEnrichmentSpanProcessor class _CollectorExporter(SpanExporter): @@ -432,69 +431,4 @@ def test_agent_version_default_empty(self) -> None: assert resolve_agent_version() == "" -class TestTraceContextMiddleware: - """Trace context middleware lifecycle behavior.""" - - def test_detach_is_deferred_until_after_app_returns(self) -> None: - events = [] - - async def app(scope, receive, send): - events.append("app-called") - - async def run_test(): - middleware = TraceContextMiddleware(app) - scope = {"type": "http", "headers": []} - - async def receive(): - return {} - - async def send(_message): - return None - - with mock.patch("azure.ai.agentserver.core._tracing._otel_context.attach", return_value="token") as attach_mock, \ - mock.patch("azure.ai.agentserver.core._tracing._otel_context.detach") as detach_mock: - await middleware(scope, receive, send) - assert events == ["app-called"] - attach_mock.assert_called_once() - detach_mock.assert_not_called() - - await asyncio.sleep(0) - detach_mock.assert_called_once_with("token") - - asyncio.run(run_test()) - - def test_detach_still_happens_when_app_raises(self) -> None: - class _AppError(RuntimeError): - pass - - async def app(scope, receive, send): - raise _AppError("boom") - - async def run_test(): - middleware = TraceContextMiddleware(app) - scope = {"type": "http", "headers": []} - - async def receive(): - return {} - - async def send(_message): - return None - - with mock.patch("azure.ai.agentserver.core._tracing._otel_context.attach", return_value="token") as attach_mock, \ - mock.patch("azure.ai.agentserver.core._tracing._otel_context.detach") as detach_mock: - try: - await middleware(scope, receive, send) - except _AppError: - pass - else: - raise AssertionError("expected middleware to propagate app exception") - - attach_mock.assert_called_once() - detach_mock.assert_not_called() - - await asyncio.sleep(0) - detach_mock.assert_called_once_with("token") - - asyncio.run(run_test()) - From f4c618e8316596ed899d7363a2ef61e66ba85bb2 Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 13:54:43 +0530 Subject: [PATCH 06/16] Harden deferred OTel detach callback Use a dedicated safe callback for call_soon detaches and keep ValueError suppression behavior for invalid/non-current tokens without regressing deferred-detach tracing fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/core/_tracing.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index 40d71c709ab4..4bfae481ee03 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -313,7 +313,7 @@ async def __call__(self, scope: Any, receive: Any, send: Any) -> None: try: await self.app(scope, receive, send) finally: - asyncio.get_running_loop().call_soon(detach_context, token) + asyncio.get_running_loop().call_soon(_safe_detach_context_callback, token) def end_span(span: Any, exc: Optional[BaseException] = None) -> None: @@ -400,14 +400,11 @@ def set_current_span(span: Any) -> Any: return _otel_context.attach(ctx) -def detach_context(token: Any) -> None: - """Detach a context previously attached by :func:`set_current_span`. - - Best-effort no-op when *token* is ``None`` or when the token is no - longer the current OpenTelemetry context. +def _safe_detach_context_callback(token: Any) -> None: + """Best-effort detach for deferred callback contexts. - :param token: The token returned by :func:`set_current_span`. - :type token: Any + This is safe to schedule via event-loop callbacks (e.g. ``call_soon``) + because invalid/non-current tokens are ignored. """ if token is not None: try: @@ -419,6 +416,18 @@ def detach_context(token: Any) -> None: ) +def detach_context(token: Any) -> None: + """Detach a context previously attached by :func:`set_current_span`. + + Best-effort no-op when *token* is ``None`` or when the token is no + longer the current OpenTelemetry context. + + :param token: The token returned by :func:`set_current_span`. + :type token: Any + """ + _safe_detach_context_callback(token) + + async def trace_stream( iterator: AsyncIterable[_Content], span: Any ) -> AsyncIterator[_Content]: From 748d9f360142a1fd172f6caf8e4d78fbd88bb7bb Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 13:56:28 +0530 Subject: [PATCH 07/16] Simplify deferred detach path Use call_soon(detach_context, token) directly and keep ValueError handling in detach_context to avoid callback noise while preserving deferred trace context behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/core/_tracing.py | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index 4bfae481ee03..40d71c709ab4 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -313,7 +313,7 @@ async def __call__(self, scope: Any, receive: Any, send: Any) -> None: try: await self.app(scope, receive, send) finally: - asyncio.get_running_loop().call_soon(_safe_detach_context_callback, token) + asyncio.get_running_loop().call_soon(detach_context, token) def end_span(span: Any, exc: Optional[BaseException] = None) -> None: @@ -400,11 +400,14 @@ def set_current_span(span: Any) -> Any: return _otel_context.attach(ctx) -def _safe_detach_context_callback(token: Any) -> None: - """Best-effort detach for deferred callback contexts. +def detach_context(token: Any) -> None: + """Detach a context previously attached by :func:`set_current_span`. + + Best-effort no-op when *token* is ``None`` or when the token is no + longer the current OpenTelemetry context. - This is safe to schedule via event-loop callbacks (e.g. ``call_soon``) - because invalid/non-current tokens are ignored. + :param token: The token returned by :func:`set_current_span`. + :type token: Any """ if token is not None: try: @@ -416,18 +419,6 @@ def _safe_detach_context_callback(token: Any) -> None: ) -def detach_context(token: Any) -> None: - """Detach a context previously attached by :func:`set_current_span`. - - Best-effort no-op when *token* is ``None`` or when the token is no - longer the current OpenTelemetry context. - - :param token: The token returned by :func:`set_current_span`. - :type token: Any - """ - _safe_detach_context_callback(token) - - async def trace_stream( iterator: AsyncIterable[_Content], span: Any ) -> AsyncIterator[_Content]: From 836b042fe0d2678d236140aca75fa84abfc5e074 Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 14:00:05 +0530 Subject: [PATCH 08/16] Detach deferred OTel token safely Use an explicit deferred callback helper that detaches the attached token and swallows ValueError to avoid unhandled callback noise while preserving delayed context cleanup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/core/_tracing.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index 40d71c709ab4..1a624c02c9a1 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -313,7 +313,7 @@ async def __call__(self, scope: Any, receive: Any, send: Any) -> None: try: await self.app(scope, receive, send) finally: - asyncio.get_running_loop().call_soon(detach_context, token) + asyncio.get_running_loop().call_soon(_detach_token_safely, token) def end_span(span: Any, exc: Optional[BaseException] = None) -> None: @@ -419,6 +419,19 @@ def detach_context(token: Any) -> None: ) +def _detach_token_safely(token: Any) -> None: + """Best-effort detach callback for deferred token cleanup.""" + if token is None: + return + try: + _otel_context.detach(token) + except ValueError: + logging.getLogger(__name__).debug( + "Ignoring OpenTelemetry context detach for a non-current token.", + exc_info=True, + ) + + async def trace_stream( iterator: AsyncIterable[_Content], span: Any ) -> AsyncIterator[_Content]: From 04ec5af5c6d1b5292444a273ff0f7e4271f1554f Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 14:03:26 +0530 Subject: [PATCH 09/16] Add fallback when call_soon scheduling fails Wrap deferred detach scheduling in try/except and fall back to immediate best-effort detach so token cleanup still happens when the event loop is closing/closed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/core/_tracing.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index 1a624c02c9a1..19dcb1ccbf66 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -313,7 +313,10 @@ async def __call__(self, scope: Any, receive: Any, send: Any) -> None: try: await self.app(scope, receive, send) finally: - asyncio.get_running_loop().call_soon(_detach_token_safely, token) + try: + asyncio.get_running_loop().call_soon(_detach_token_safely, token) + except RuntimeError: + _detach_token_safely(token) def end_span(span: Any, exc: Optional[BaseException] = None) -> None: From 98419e46256ac486b6d817c7493f1986bcdd16e5 Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 14:05:38 +0530 Subject: [PATCH 10/16] Reuse detach_context for deferred cleanup Route both call_soon callback and scheduling-failure fallback through detach_context(token) to keep detach semantics centralized and consistent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/core/_tracing.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index 19dcb1ccbf66..7adcb8a86b7f 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -314,9 +314,9 @@ async def __call__(self, scope: Any, receive: Any, send: Any) -> None: await self.app(scope, receive, send) finally: try: - asyncio.get_running_loop().call_soon(_detach_token_safely, token) + asyncio.get_running_loop().call_soon(detach_context, token) except RuntimeError: - _detach_token_safely(token) + detach_context(token) def end_span(span: Any, exc: Optional[BaseException] = None) -> None: @@ -422,19 +422,6 @@ def detach_context(token: Any) -> None: ) -def _detach_token_safely(token: Any) -> None: - """Best-effort detach callback for deferred token cleanup.""" - if token is None: - return - try: - _otel_context.detach(token) - except ValueError: - logging.getLogger(__name__).debug( - "Ignoring OpenTelemetry context detach for a non-current token.", - exc_info=True, - ) - - async def trace_stream( iterator: AsyncIterable[_Content], span: Any ) -> AsyncIterator[_Content]: From 3a8134a33d986481b16fa27ad577651605058ad7 Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 14:29:00 +0530 Subject: [PATCH 11/16] Enrich logs with agent and session dimensions Add agent name/version and sessionid dimensions to log records via OTel log processor while retaining baggage propagation and span enrichment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/core/_tracing.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index 7adcb8a86b7f..c12313b3199f 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -54,6 +54,7 @@ _ATTR_GEN_AI_AGENT_TENANT_ID = "microsoft.tenant.id" _ATTR_GEN_AI_AGENT_NAME = "gen_ai.agent.name" _ATTR_GEN_AI_AGENT_VERSION = "gen_ai.agent.version" +_ATTR_GEN_AI_AGENT_SESSIONID = "gen_ai.agent.sessionid" _ATTR_GEN_AI_RESPONSE_ID = "gen_ai.response.id" _ATTR_GEN_AI_OPERATION_NAME = "gen_ai.operation.name" _ATTR_GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" @@ -183,7 +184,12 @@ def _configure_tracing(connection_string: Optional[str] = None, enable_sensitive agent_tenant_id=agent_tenant_id, ), ] - log_record_processors = [_BaggageLogRecordProcessor()] # type: ignore[list-item] + log_record_processors = [ + _BaggageLogRecordProcessor( + agent_name=agent_name, + agent_version=agent_version, + ) + ] # type: ignore[list-item] try: _setup_distro_export( @@ -484,6 +490,7 @@ def on_start(self, span: Any, parent_context: Any = None) -> None: session_id = _otel_baggage.get_baggage(_BAGGAGE_SESSION_ID, context=ctx) if session_id: span.set_attribute(_ATTR_SESSION_ID, session_id) + span.set_attribute(_ATTR_GEN_AI_AGENT_SESSIONID, session_id) conversation_id = _otel_baggage.get_baggage(_BAGGAGE_CONVERSATION_ID, context=ctx) if conversation_id: span.set_attribute(_ATTR_GEN_AI_CONVERSATION_ID, conversation_id) @@ -536,6 +543,15 @@ class _BaggageLogRecordProcessor: for end-to-end correlation. """ + def __init__( + self, + *, + agent_name: Optional[str] = None, + agent_version: Optional[str] = None, + ) -> None: + self.agent_name = agent_name + self.agent_version = agent_version + def on_emit(self, log_data: Any) -> None: # pylint: disable=unused-argument """Copy baggage entries into the log record's attributes. @@ -545,6 +561,15 @@ def on_emit(self, log_data: Any) -> None: # pylint: disable=unused-argument try: ctx = _otel_context.get_current() entries = _otel_baggage.get_all(context=ctx) + if hasattr(log_data, 'log_record') and log_data.log_record: + attrs = log_data.log_record.attributes + if self.agent_name: + attrs[_ATTR_GEN_AI_AGENT_NAME] = self.agent_name # type: ignore[index] + if self.agent_version: + attrs[_ATTR_GEN_AI_AGENT_VERSION] = self.agent_version # type: ignore[index] + session_id = _otel_baggage.get_baggage(_BAGGAGE_SESSION_ID, context=ctx) + if session_id: + attrs[_ATTR_GEN_AI_AGENT_SESSIONID] = session_id # type: ignore[index] if entries and hasattr(log_data, 'log_record') and log_data.log_record: for key, value in entries.items(): log_data.log_record.attributes[key] = value # type: ignore[index] From 950a618987859ea6ed70b4934eee1e7bed0515a8 Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 14:43:33 +0530 Subject: [PATCH 12/16] Fix log enrichment fallback and drop sessionid dimension Resolve agent name/version from AGENT_* env pairs when FOUNDRY_AGENT_* is absent, and remove gen_ai.agent.sessionid to rely on existing azure.ai.agentserver.session_id baggage field. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/core/_tracing.py | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index c12313b3199f..2f75d92df6a6 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -54,7 +54,6 @@ _ATTR_GEN_AI_AGENT_TENANT_ID = "microsoft.tenant.id" _ATTR_GEN_AI_AGENT_NAME = "gen_ai.agent.name" _ATTR_GEN_AI_AGENT_VERSION = "gen_ai.agent.version" -_ATTR_GEN_AI_AGENT_SESSIONID = "gen_ai.agent.sessionid" _ATTR_GEN_AI_RESPONSE_ID = "gen_ai.response.id" _ATTR_GEN_AI_OPERATION_NAME = "gen_ai.operation.name" _ATTR_GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" @@ -173,6 +172,14 @@ def _configure_tracing(connection_string: Optional[str] = None, enable_sensitive agent_version = _config.resolve_agent_version() or None project_id = _config.resolve_project_id() or None agent_id = _config.resolve_agent_id() or None + # Hosted runtime can expose agent identity as AGENT__NAME / _VERSION + # rather than FOUNDRY_AGENT_NAME / _VERSION. Fill missing fields from that + # environment shape to ensure log enrichment has stable dimensions. + env_agent_name, env_agent_version = _resolve_agent_name_version_from_env() + if not agent_name: + agent_name = env_agent_name + if not agent_version: + agent_version = env_agent_version agent_blueprint_id = _config.resolve_agent_blueprint_id() or None agent_tenant_id = _config.resolve_agent_tenant_id() or None @@ -490,7 +497,6 @@ def on_start(self, span: Any, parent_context: Any = None) -> None: session_id = _otel_baggage.get_baggage(_BAGGAGE_SESSION_ID, context=ctx) if session_id: span.set_attribute(_ATTR_SESSION_ID, session_id) - span.set_attribute(_ATTR_GEN_AI_AGENT_SESSIONID, session_id) conversation_id = _otel_baggage.get_baggage(_BAGGAGE_CONVERSATION_ID, context=ctx) if conversation_id: span.set_attribute(_ATTR_GEN_AI_CONVERSATION_ID, conversation_id) @@ -567,9 +573,6 @@ def on_emit(self, log_data: Any) -> None: # pylint: disable=unused-argument attrs[_ATTR_GEN_AI_AGENT_NAME] = self.agent_name # type: ignore[index] if self.agent_version: attrs[_ATTR_GEN_AI_AGENT_VERSION] = self.agent_version # type: ignore[index] - session_id = _otel_baggage.get_baggage(_BAGGAGE_SESSION_ID, context=ctx) - if session_id: - attrs[_ATTR_GEN_AI_AGENT_SESSIONID] = session_id # type: ignore[index] if entries and hasattr(log_data, 'log_record') and log_data.log_record: for key, value in entries.items(): log_data.log_record.attributes[key] = value # type: ignore[index] @@ -600,6 +603,32 @@ def _create_resource() -> Any: return Resource.create({_ATTR_SERVICE_NAME: service_name}) +def _resolve_agent_name_version_from_env() -> tuple[Optional[str], Optional[str]]: + """Resolve agent name/version from AGENT_*_NAME / AGENT_*_VERSION env pairs.""" + stems_to_name: dict[str, str] = {} + stems_to_version: dict[str, str] = {} + for key, value in os.environ.items(): + if not value: + continue + if key.startswith("AGENT_") and key.endswith("_NAME"): + stem = key[len("AGENT_"):-len("_NAME")] + stems_to_name[stem] = value + elif key.startswith("AGENT_") and key.endswith("_VERSION"): + stem = key[len("AGENT_"):-len("_VERSION")] + stems_to_version[stem] = value + + # Prefer a matched pair from the same stem. + for stem, name in stems_to_name.items(): + version = stems_to_version.get(stem) + if name or version: + return (name or None, version or None) + + # Fallback to any available values. + any_name = next(iter(stems_to_name.values()), None) + any_version = next(iter(stems_to_version.values()), None) + return any_name, any_version + + def _ensure_trace_provider(resource: Any, span_processors: Optional[list[Any]] = None) -> Any: """Get or create a TracerProvider, optionally adding span processors. From 5243eb264daa6e80ce532b1328b9f8ec1f66ceb4 Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 15:47:26 +0530 Subject: [PATCH 13/16] Fix trace context detach and agent identity resolution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/core/_config.py | 65 ++++++++++++++----- .../azure/ai/agentserver/core/_tracing.py | 47 ++------------ 2 files changed, 54 insertions(+), 58 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py index f1d8dd3db86f..2acac64e0551 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py @@ -109,15 +109,9 @@ def from_env(cls) -> Self: :return: A frozen config with resolved values. :rtype: AgentConfig """ - agent_name = os.environ.get(_ENV_FOUNDRY_AGENT_NAME, "") - agent_version = os.environ.get(_ENV_FOUNDRY_AGENT_VERSION, "") - - if agent_name and agent_version: - agent_id = f"{agent_name}:{agent_version}" - elif agent_name: - agent_id = agent_name - else: - agent_id = "" + agent_name = resolve_agent_name() + agent_version = resolve_agent_version() + agent_id = resolve_agent_id() return cls( agent_name=agent_name, @@ -275,22 +269,63 @@ def resolve_log_level(level: Optional[str]) -> str: return normalized +def _resolve_agent_name_version_from_hosted_env() -> tuple[str, str]: + """Resolve agent name/version from ``AGENT__NAME`` and ``AGENT__VERSION`` env pairs.""" + stems_to_name: dict[str, str] = {} + stems_to_version: dict[str, str] = {} + + for key, value in os.environ.items(): + if not value: + continue + if key.startswith("AGENT_") and key.endswith("_NAME"): + stem = key[len("AGENT_"):-len("_NAME")] + stems_to_name[stem] = value + elif key.startswith("AGENT_") and key.endswith("_VERSION"): + stem = key[len("AGENT_"):-len("_VERSION")] + stems_to_version[stem] = value + + for stem, name in stems_to_name.items(): + version = stems_to_version.get(stem, "") + if name or version: + return name, version + + any_name = next(iter(stems_to_name.values()), "") + any_version = next(iter(stems_to_version.values()), "") + return any_name, any_version + + def resolve_agent_name() -> str: - """Resolve the agent name from the ``FOUNDRY_AGENT_NAME`` environment variable. + """Resolve the agent name from environment variables. + + Resolution order: + 1. ``FOUNDRY_AGENT_NAME`` + 2. Hosted runtime fallback from ``AGENT__NAME`` :return: The agent name, or an empty string if not set. :rtype: str """ - return os.environ.get(_ENV_FOUNDRY_AGENT_NAME, "") + agent_name = os.environ.get(_ENV_FOUNDRY_AGENT_NAME, "") + if agent_name: + return agent_name + hosted_agent_name, _ = _resolve_agent_name_version_from_hosted_env() + return hosted_agent_name def resolve_agent_version() -> str: - """Resolve the agent version from the ``FOUNDRY_AGENT_VERSION`` environment variable. + """Resolve the agent version from environment variables. + + Resolution order: + 1. ``FOUNDRY_AGENT_VERSION`` + 2. Hosted runtime fallback from ``AGENT__VERSION`` :return: The agent version, or an empty string if not set. :rtype: str """ - return os.environ.get(_ENV_FOUNDRY_AGENT_VERSION, "") + agent_version = os.environ.get(_ENV_FOUNDRY_AGENT_VERSION, "") + if agent_version: + return agent_version + _, hosted_agent_version = _resolve_agent_name_version_from_hosted_env() + return hosted_agent_version def resolve_agent_id() -> str: @@ -308,8 +343,8 @@ def resolve_agent_id() -> str: agent_id = os.environ.get(_ENV_FOUNDRY_AGENT_INSTANCE_CLIENT_ID, "") if agent_id: return agent_id - agent_name = os.environ.get(_ENV_FOUNDRY_AGENT_NAME, "") - agent_version = os.environ.get(_ENV_FOUNDRY_AGENT_VERSION, "") + agent_name = resolve_agent_name() + agent_version = resolve_agent_version() if agent_name and agent_version: return f"{agent_name}:{agent_version}" return agent_name diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index 2f75d92df6a6..d13227e72996 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -33,7 +33,6 @@ OpenTelemetry is a required dependency — these functions always create real spans. Azure Monitor export is optional (auto-configured by the distro). """ -import asyncio import logging import os from collections.abc import AsyncIterable, AsyncIterator # pylint: disable=import-error @@ -172,14 +171,6 @@ def _configure_tracing(connection_string: Optional[str] = None, enable_sensitive agent_version = _config.resolve_agent_version() or None project_id = _config.resolve_project_id() or None agent_id = _config.resolve_agent_id() or None - # Hosted runtime can expose agent identity as AGENT__NAME / _VERSION - # rather than FOUNDRY_AGENT_NAME / _VERSION. Fill missing fields from that - # environment shape to ensure log enrichment has stable dimensions. - env_agent_name, env_agent_version = _resolve_agent_name_version_from_env() - if not agent_name: - agent_name = env_agent_name - if not agent_version: - agent_version = env_agent_version agent_blueprint_id = _config.resolve_agent_blueprint_id() or None agent_tenant_id = _config.resolve_agent_tenant_id() or None @@ -319,17 +310,13 @@ async def __call__(self, scope: Any, receive: Any, send: Any) -> None: non_recording = trace.NonRecordingSpan(span_ctx) ctx = trace.set_span_in_context(non_recording, ctx) - # Attach request context for app execution and defer detach by one - # event-loop turn so server-side access logging that runs immediately - # after app return can still observe the request trace context. + # Attach request context for app execution and detach in the same + # Task/context to avoid leaking contextvars across requests. token = _otel_context.attach(ctx) try: await self.app(scope, receive, send) finally: - try: - asyncio.get_running_loop().call_soon(detach_context, token) - except RuntimeError: - detach_context(token) + detach_context(token) def end_span(span: Any, exc: Optional[BaseException] = None) -> None: @@ -598,37 +585,11 @@ def _create_resource() -> Any: logger.warning("OTel SDK not installed — tracing resource creation failed.") return None # service.name maps to cloud_RoleName in App Insights - agent_name = os.environ.get(_config._ENV_FOUNDRY_AGENT_NAME, "") # pylint: disable=protected-access + agent_name = _config.resolve_agent_name() or "" service_name = agent_name or _SERVICE_NAME_VALUE return Resource.create({_ATTR_SERVICE_NAME: service_name}) -def _resolve_agent_name_version_from_env() -> tuple[Optional[str], Optional[str]]: - """Resolve agent name/version from AGENT_*_NAME / AGENT_*_VERSION env pairs.""" - stems_to_name: dict[str, str] = {} - stems_to_version: dict[str, str] = {} - for key, value in os.environ.items(): - if not value: - continue - if key.startswith("AGENT_") and key.endswith("_NAME"): - stem = key[len("AGENT_"):-len("_NAME")] - stems_to_name[stem] = value - elif key.startswith("AGENT_") and key.endswith("_VERSION"): - stem = key[len("AGENT_"):-len("_VERSION")] - stems_to_version[stem] = value - - # Prefer a matched pair from the same stem. - for stem, name in stems_to_name.items(): - version = stems_to_version.get(stem) - if name or version: - return (name or None, version or None) - - # Fallback to any available values. - any_name = next(iter(stems_to_name.values()), None) - any_version = next(iter(stems_to_version.values()), None) - return any_name, any_version - - def _ensure_trace_provider(resource: Any, span_processors: Optional[list[Any]] = None) -> Any: """Get or create a TracerProvider, optionally adding span processors. From c0e39a3ec11b91c36c3403077d78fc9d2488f9bb Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 16:18:46 +0530 Subject: [PATCH 14/16] Add AZURE_AI_* agent identity fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/core/_config.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py index 2acac64e0551..38bda837f432 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py @@ -24,6 +24,8 @@ _ENV_FOUNDRY_AGENT_NAME = "FOUNDRY_AGENT_NAME" _ENV_FOUNDRY_AGENT_VERSION = "FOUNDRY_AGENT_VERSION" +_ENV_AZURE_AI_AGENT_NAME = "AZURE_AI_AGENT_NAME" +_ENV_AZURE_AI_AGENT_VERSION = "AZURE_AI_AGENT_VERSION" _ENV_FOUNDRY_AGENT_INSTANCE_CLIENT_ID = "FOUNDRY_AGENT_INSTANCE_CLIENT_ID" _ENV_FOUNDRY_AGENT_BLUEPRINT_CLIENT_ID = "FOUNDRY_AGENT_BLUEPRINT_CLIENT_ID" _ENV_FOUNDRY_AGENT_TENANT_ID = "FOUNDRY_AGENT_TENANT_ID" @@ -299,12 +301,16 @@ def resolve_agent_name() -> str: Resolution order: 1. ``FOUNDRY_AGENT_NAME`` - 2. Hosted runtime fallback from ``AGENT__NAME`` + 2. ``AZURE_AI_AGENT_NAME`` + 3. Hosted runtime fallback from ``AGENT__NAME`` :return: The agent name, or an empty string if not set. :rtype: str """ agent_name = os.environ.get(_ENV_FOUNDRY_AGENT_NAME, "") + if agent_name: + return agent_name + agent_name = os.environ.get(_ENV_AZURE_AI_AGENT_NAME, "") if agent_name: return agent_name hosted_agent_name, _ = _resolve_agent_name_version_from_hosted_env() @@ -316,12 +322,16 @@ def resolve_agent_version() -> str: Resolution order: 1. ``FOUNDRY_AGENT_VERSION`` - 2. Hosted runtime fallback from ``AGENT__VERSION`` + 2. ``AZURE_AI_AGENT_VERSION`` + 3. Hosted runtime fallback from ``AGENT__VERSION`` :return: The agent version, or an empty string if not set. :rtype: str """ agent_version = os.environ.get(_ENV_FOUNDRY_AGENT_VERSION, "") + if agent_version: + return agent_version + agent_version = os.environ.get(_ENV_AZURE_AI_AGENT_VERSION, "") if agent_version: return agent_version _, hosted_agent_version = _resolve_agent_name_version_from_hosted_env() From f2ab16baccf20eabfbfd78dceefecd5cf480b39e Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 16:25:41 +0530 Subject: [PATCH 15/16] Revert agent name/version fallback changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/core/_config.py | 63 ++----------------- 1 file changed, 6 insertions(+), 57 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py index 38bda837f432..20f862b1fe20 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py @@ -24,8 +24,6 @@ _ENV_FOUNDRY_AGENT_NAME = "FOUNDRY_AGENT_NAME" _ENV_FOUNDRY_AGENT_VERSION = "FOUNDRY_AGENT_VERSION" -_ENV_AZURE_AI_AGENT_NAME = "AZURE_AI_AGENT_NAME" -_ENV_AZURE_AI_AGENT_VERSION = "AZURE_AI_AGENT_VERSION" _ENV_FOUNDRY_AGENT_INSTANCE_CLIENT_ID = "FOUNDRY_AGENT_INSTANCE_CLIENT_ID" _ENV_FOUNDRY_AGENT_BLUEPRINT_CLIENT_ID = "FOUNDRY_AGENT_BLUEPRINT_CLIENT_ID" _ENV_FOUNDRY_AGENT_TENANT_ID = "FOUNDRY_AGENT_TENANT_ID" @@ -271,71 +269,22 @@ def resolve_log_level(level: Optional[str]) -> str: return normalized -def _resolve_agent_name_version_from_hosted_env() -> tuple[str, str]: - """Resolve agent name/version from ``AGENT__NAME`` and ``AGENT__VERSION`` env pairs.""" - stems_to_name: dict[str, str] = {} - stems_to_version: dict[str, str] = {} - - for key, value in os.environ.items(): - if not value: - continue - if key.startswith("AGENT_") and key.endswith("_NAME"): - stem = key[len("AGENT_"):-len("_NAME")] - stems_to_name[stem] = value - elif key.startswith("AGENT_") and key.endswith("_VERSION"): - stem = key[len("AGENT_"):-len("_VERSION")] - stems_to_version[stem] = value - - for stem, name in stems_to_name.items(): - version = stems_to_version.get(stem, "") - if name or version: - return name, version - - any_name = next(iter(stems_to_name.values()), "") - any_version = next(iter(stems_to_version.values()), "") - return any_name, any_version - - def resolve_agent_name() -> str: - """Resolve the agent name from environment variables. - - Resolution order: - 1. ``FOUNDRY_AGENT_NAME`` - 2. ``AZURE_AI_AGENT_NAME`` - 3. Hosted runtime fallback from ``AGENT__NAME`` + """Resolve the agent name from the ``FOUNDRY_AGENT_NAME`` environment variable. :return: The agent name, or an empty string if not set. :rtype: str """ - agent_name = os.environ.get(_ENV_FOUNDRY_AGENT_NAME, "") - if agent_name: - return agent_name - agent_name = os.environ.get(_ENV_AZURE_AI_AGENT_NAME, "") - if agent_name: - return agent_name - hosted_agent_name, _ = _resolve_agent_name_version_from_hosted_env() - return hosted_agent_name + return os.environ.get(_ENV_FOUNDRY_AGENT_NAME, "") def resolve_agent_version() -> str: - """Resolve the agent version from environment variables. - - Resolution order: - 1. ``FOUNDRY_AGENT_VERSION`` - 2. ``AZURE_AI_AGENT_VERSION`` - 3. Hosted runtime fallback from ``AGENT__VERSION`` + """Resolve the agent version from the ``FOUNDRY_AGENT_VERSION`` environment variable. :return: The agent version, or an empty string if not set. :rtype: str """ - agent_version = os.environ.get(_ENV_FOUNDRY_AGENT_VERSION, "") - if agent_version: - return agent_version - agent_version = os.environ.get(_ENV_AZURE_AI_AGENT_VERSION, "") - if agent_version: - return agent_version - _, hosted_agent_version = _resolve_agent_name_version_from_hosted_env() - return hosted_agent_version + return os.environ.get(_ENV_FOUNDRY_AGENT_VERSION, "") def resolve_agent_id() -> str: @@ -353,8 +302,8 @@ def resolve_agent_id() -> str: agent_id = os.environ.get(_ENV_FOUNDRY_AGENT_INSTANCE_CLIENT_ID, "") if agent_id: return agent_id - agent_name = resolve_agent_name() - agent_version = resolve_agent_version() + agent_name = os.environ.get(_ENV_FOUNDRY_AGENT_NAME, "") + agent_version = os.environ.get(_ENV_FOUNDRY_AGENT_VERSION, "") if agent_name and agent_version: return f"{agent_name}:{agent_version}" return agent_name From ab46ec2ef622c98a14628dcf0ebab9518cfe043b Mon Sep 17 00:00:00 2001 From: Harsheet Shah Date: Mon, 8 Jun 2026 16:31:09 +0530 Subject: [PATCH 16/16] Revert agent name/version enrichment changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/ai/agentserver/core/_config.py | 12 +++++++--- .../azure/ai/agentserver/core/_tracing.py | 24 ++----------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py index 20f862b1fe20..f1d8dd3db86f 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py @@ -109,9 +109,15 @@ def from_env(cls) -> Self: :return: A frozen config with resolved values. :rtype: AgentConfig """ - agent_name = resolve_agent_name() - agent_version = resolve_agent_version() - agent_id = resolve_agent_id() + agent_name = os.environ.get(_ENV_FOUNDRY_AGENT_NAME, "") + agent_version = os.environ.get(_ENV_FOUNDRY_AGENT_VERSION, "") + + if agent_name and agent_version: + agent_id = f"{agent_name}:{agent_version}" + elif agent_name: + agent_id = agent_name + else: + agent_id = "" return cls( agent_name=agent_name, diff --git a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py index d13227e72996..b580ac2504a6 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py +++ b/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_tracing.py @@ -182,12 +182,7 @@ def _configure_tracing(connection_string: Optional[str] = None, enable_sensitive agent_tenant_id=agent_tenant_id, ), ] - log_record_processors = [ - _BaggageLogRecordProcessor( - agent_name=agent_name, - agent_version=agent_version, - ) - ] # type: ignore[list-item] + log_record_processors = [_BaggageLogRecordProcessor()] # type: ignore[list-item] try: _setup_distro_export( @@ -536,15 +531,6 @@ class _BaggageLogRecordProcessor: for end-to-end correlation. """ - def __init__( - self, - *, - agent_name: Optional[str] = None, - agent_version: Optional[str] = None, - ) -> None: - self.agent_name = agent_name - self.agent_version = agent_version - def on_emit(self, log_data: Any) -> None: # pylint: disable=unused-argument """Copy baggage entries into the log record's attributes. @@ -554,12 +540,6 @@ def on_emit(self, log_data: Any) -> None: # pylint: disable=unused-argument try: ctx = _otel_context.get_current() entries = _otel_baggage.get_all(context=ctx) - if hasattr(log_data, 'log_record') and log_data.log_record: - attrs = log_data.log_record.attributes - if self.agent_name: - attrs[_ATTR_GEN_AI_AGENT_NAME] = self.agent_name # type: ignore[index] - if self.agent_version: - attrs[_ATTR_GEN_AI_AGENT_VERSION] = self.agent_version # type: ignore[index] if entries and hasattr(log_data, 'log_record') and log_data.log_record: for key, value in entries.items(): log_data.log_record.attributes[key] = value # type: ignore[index] @@ -585,7 +565,7 @@ def _create_resource() -> Any: logger.warning("OTel SDK not installed — tracing resource creation failed.") return None # service.name maps to cloud_RoleName in App Insights - agent_name = _config.resolve_agent_name() or "" + agent_name = os.environ.get(_config._ENV_FOUNDRY_AGENT_NAME, "") # pylint: disable=protected-access service_name = agent_name or _SERVICE_NAME_VALUE return Resource.create({_ATTR_SERVICE_NAME: service_name})