From ec23bd3528f67c5d132120555f2959a3e2a17a56 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Fri, 2 Jan 2026 21:40:02 +0530 Subject: [PATCH 1/7] ensure exporter is added if reusing the tracer provider --- .../observability/core/config.py | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py index 4f10da10..0bc69095 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py @@ -96,6 +96,13 @@ def _configure_internal( ) -> bool: """Internal configuration method - not thread-safe, must be called with lock.""" + # Check if a365 observability is already configured + if self._tracer_provider is not None: + self._logger.warning( + "a365 observability already configured. Ignoring repeated configure() call. " + ) + return True + # Create resource with service information resource = Resource.create( { @@ -104,23 +111,24 @@ def _configure_internal( } ) - # Get existing tracer provider or create new one - try: - tracer_provider = trace.get_tracer_provider() - # Check if it's already configured - if hasattr(tracer_provider, "resource") and tracer_provider.resource: - # Already configured, just add our span processor - agent_processor = SpanProcessor() - tracer_provider.add_span_processor(agent_processor) - self._tracer_provider = tracer_provider - self._span_processors["agent"] = agent_processor - return True - except Exception: - pass - - # Configure tracer provider - tracer_provider = TracerProvider(resource=resource) - trace.set_tracer_provider(tracer_provider) + # Check if there's an existing TracerProvider (from app's OTEL setup) + tracer_provider = trace.get_tracer_provider() + + # Determine if we should use existing provider or create new one + # Check if it's a real TracerProvider with a resource (not a proxy/no-op) + if hasattr(tracer_provider, "resource") and tracer_provider.resource: + # Use existing provider from application's OTEL setup + self._logger.info( + "Detected existing TracerProvider with resource. " + "Adding a365 observability processors to it." + ) + else: + # Create new TracerProvider with our resource + self._logger.info("Creating new TracerProvider for a365 observability.") + tracer_provider = TracerProvider(resource=resource) + trace.set_tracer_provider(tracer_provider) + + # Store reference self._tracer_provider = tracer_provider # Use exporter_options if provided, otherwise create default options with legacy parameters From 140ce27177f80c7e93db1be0b21f650dad32b4cd Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Mon, 5 Jan 2026 17:01:45 +0530 Subject: [PATCH 2/7] Mak te A365exporter internal and final so its not instantiated outside the core sdk --- .../microsoft_agents_a365/observability/core/config.py | 4 ++-- .../observability/core/exporters/__init__py | 3 +++ .../observability/core/exporters/agent365_exporter.py | 5 +++-- tests/observability/core/test_agent365.py | 4 ++-- tests/observability/core/test_agent365_exporter.py | 8 ++++---- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py index 0bc69095..6603dce0 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py @@ -10,7 +10,7 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter -from .exporters.agent365_exporter import Agent365Exporter +from .exporters.agent365_exporter import _Agent365Exporter from .exporters.agent365_exporter_options import Agent365ExporterOptions from .exporters.utils import is_agent365_exporter_enabled from .trace_processor.span_processor import SpanProcessor @@ -147,7 +147,7 @@ def _configure_internal( } if is_agent365_exporter_enabled() and exporter_options.token_resolver is not None: - exporter = Agent365Exporter( + exporter = _Agent365Exporter( token_resolver=exporter_options.token_resolver, cluster_category=exporter_options.cluster_category, use_s2s_endpoint=exporter_options.use_s2s_endpoint, diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__py index e69de29b..2867652c 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__py @@ -0,0 +1,3 @@ +# Agent365Exporter is not exported intentionally. +# It should only be used internally by the observability core module. +__all__ = ["Agent365ExporterOptions"] diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py index 2824416e..e2018c75 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py @@ -9,7 +9,7 @@ import threading import time from collections.abc import Callable, Sequence -from typing import Any +from typing import Any, final import requests from microsoft_agents_a365.runtime.power_platform_api_discovery import PowerPlatformApiDiscovery @@ -36,7 +36,8 @@ logger = logging.getLogger(__name__) -class Agent365Exporter(SpanExporter): +@final +class _Agent365Exporter(SpanExporter): """ Agent 365 span exporter for Agent 365: * Partitions spans by (tenantId, agentId) diff --git a/tests/observability/core/test_agent365.py b/tests/observability/core/test_agent365.py index 2ebde6ad..f9009dae 100644 --- a/tests/observability/core/test_agent365.py +++ b/tests/observability/core/test_agent365.py @@ -61,13 +61,13 @@ def test_configure_with_exporter_options_and_parameter_precedence(self, mock_is_ ) self.assertTrue(result, "configure() should return True with exporter_options") - @patch("microsoft_agents_a365.observability.core.config.Agent365Exporter") + @patch("microsoft_agents_a365.observability.core.config._Agent365Exporter") @patch("microsoft_agents_a365.observability.core.config.BatchSpanProcessor") @patch("microsoft_agents_a365.observability.core.config.is_agent365_exporter_enabled") def test_batch_span_processor_and_exporter_called_with_correct_values( self, mock_is_enabled, mock_batch_processor, mock_exporter ): - """Test that BatchSpanProcessor and Agent365Exporter are called with correct values from exporter_options.""" + """Test that BatchSpanProcessor and _Agent365Exporter are called with correct values from exporter_options.""" # Enable Agent365 exporter for this test mock_is_enabled.return_value = True diff --git a/tests/observability/core/test_agent365_exporter.py b/tests/observability/core/test_agent365_exporter.py index e157f6c7..b272f08c 100644 --- a/tests/observability/core/test_agent365_exporter.py +++ b/tests/observability/core/test_agent365_exporter.py @@ -6,7 +6,7 @@ from microsoft_agents_a365.observability.core.constants import GEN_AI_AGENT_ID_KEY, TENANT_ID_KEY from microsoft_agents_a365.observability.core.exporters.agent365_exporter import ( - Agent365Exporter, + _Agent365Exporter, ) from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExportResult @@ -21,7 +21,7 @@ def setUp(self): self.mock_token_resolver.return_value = "test_token_123" # Don't patch the class in setUp, do it per test - self.exporter = Agent365Exporter( + self.exporter = _Agent365Exporter( token_resolver=self.mock_token_resolver, cluster_category="test" ) @@ -216,7 +216,7 @@ def test_partitioning_by_scope(self): def test_s2s_endpoint_path_when_enabled(self): """Test 4: Test that S2S endpoint path is used when use_s2s_endpoint is True.""" # Arrange - Create exporter with S2S endpoint enabled - s2s_exporter = Agent365Exporter( + s2s_exporter = _Agent365Exporter( token_resolver=self.mock_token_resolver, cluster_category="test", use_s2s_endpoint=True ) @@ -252,7 +252,7 @@ def test_s2s_endpoint_path_when_enabled(self): def test_default_endpoint_path_when_s2s_disabled(self): """Test 5: Test that default endpoint path is used when use_s2s_endpoint is False.""" # Arrange - Create exporter with S2S endpoint disabled (default behavior) - default_exporter = Agent365Exporter( + default_exporter = _Agent365Exporter( token_resolver=self.mock_token_resolver, cluster_category="test", use_s2s_endpoint=False ) From 4099769f954673a75cb17a4f811e373d83fc1b64 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Mon, 5 Jan 2026 17:35:18 +0530 Subject: [PATCH 3/7] add tests --- tests/observability/core/test_agent365.py | 112 ++++++++++++++++++ .../core/test_agent365_exporter.py | 12 ++ 2 files changed, 124 insertions(+) diff --git a/tests/observability/core/test_agent365.py b/tests/observability/core/test_agent365.py index f9009dae..ba6ab0ad 100644 --- a/tests/observability/core/test_agent365.py +++ b/tests/observability/core/test_agent365.py @@ -9,6 +9,9 @@ Agent365ExporterOptions, ) from microsoft_agents_a365.observability.core.trace_processor import SpanProcessor +from opentelemetry import trace as otel_trace +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider class TestAgent365Configure(unittest.TestCase): @@ -19,6 +22,16 @@ def setUp(self): self.mock_token_resolver = Mock() self.mock_token_resolver.return_value = "test_token_123" + def tearDown(self): + """Clean up after each test.""" + # Reset the telemetry manager singleton state + from microsoft_agents_a365.observability.core.config import _telemetry_manager + + _telemetry_manager._tracer_provider = None + _telemetry_manager._span_processors = {} + + otel_trace._TRACER_PROVIDER = None + def test_configure_basic_functionality(self): """Test configure function with basic parameters and legacy parameters.""" # Test basic configuration without exporter_options @@ -112,6 +125,105 @@ def test_span_processor_creation(self): processor = SpanProcessor() self.assertIsNotNone(processor, "SpanProcessor should be created successfully") + def test_configure_prevents_duplicate_initialization(self): + """Test that calling configure() multiple times doesn't reinitialize.""" + result1 = configure( + service_name="test-service-1", + service_namespace="test-namespace-1", + ) + self.assertTrue(result1) + + with patch( + "microsoft_agents_a365.observability.core.config._telemetry_manager._logger" + ) as mock_logger: + result2 = configure( + service_name="test-service-2", + service_namespace="test-namespace-2", + ) + self.assertTrue(result2) + mock_logger.warning.assert_called_once() + self.assertIn("already configured", mock_logger.warning.call_args[0][0].lower()) + + @patch("microsoft_agents_a365.observability.core.config.is_agent365_exporter_enabled") + @patch("microsoft_agents_a365.observability.core.config.TracerProvider") + def test_configure_creates_new_tracer_provider(self, mock_provider_class, mock_is_enabled): + """Test configure() creates new TracerProvider when none exists and adds processors.""" + mock_is_enabled.return_value = False + + new_provider = TracerProvider( + resource=Resource.create({ + "service.name": "test-service", + "service.namespace": "test-namespace", + }) + ) + mock_provider_class.return_value = new_provider + + result = configure(service_name="test-service", service_namespace="test-namespace") + self.assertTrue(result) + + # Verify both processors were added by inspecting the MultiSpanProcessor + + active_processor = new_provider._active_span_processor + self.assertIsNotNone(active_processor) + + # MultiSpanProcessor has a _span_processors list + processors = active_processor._span_processors + self.assertEqual( + len(processors), 2, "Should have 2 processors: BatchSpanProcessor and SpanProcessor" + ) + + # Verify types of processors + processor_types = [type(p).__name__ for p in processors] + self.assertIn("BatchSpanProcessor", processor_types) + self.assertIn("SpanProcessor", processor_types) + + @patch("microsoft_agents_a365.observability.core.config.is_agent365_exporter_enabled") + @patch("microsoft_agents_a365.observability.core.config.trace.get_tracer_provider") + def test_configure_uses_existing_tracer_provider(self, mock_get_provider, mock_is_enabled): + """Test configure() uses existing TracerProvider and adds processors without calling set_tracer_provider.""" + mock_is_enabled.return_value = False + + existing_provider = TracerProvider( + resource=Resource.create({"service.name": "existing-service"}) + ) + mock_get_provider.return_value = existing_provider + + with patch( + "microsoft_agents_a365.observability.core.config._telemetry_manager._logger" + ) as mock_logger: + with patch( + "microsoft_agents_a365.observability.core.config.trace.set_tracer_provider" + ) as mock_set: + result = configure(service_name="new-service", service_namespace="new-namespace") + self.assertTrue(result) + + # Verify existing provider was detected + info_calls = [call[0][0] for call in mock_logger.info.call_args_list] + self.assertTrue( + any("Detected existing TracerProvider" in msg for msg in info_calls) + ) + + # Verify didn't call set_tracer_provider + mock_set.assert_not_called() + + # Verify both processors were added by inspecting the MultiSpanProcessor + + active_processor = existing_provider._active_span_processor + self.assertIsNotNone(active_processor) + + # MultiSpanProcessor has a _span_processors list + processors = active_processor._span_processors + self.assertEqual( + len(processors), + 2, + "Should have 2 processors: BatchSpanProcessor and SpanProcessor", + ) + + # Verify types of processors + processor_types = [type(p).__name__ for p in processors] + self.assertIn("BatchSpanProcessor", processor_types) + self.assertIn("SpanProcessor", processor_types) + if __name__ == "__main__": unittest.main() diff --git a/tests/observability/core/test_agent365_exporter.py b/tests/observability/core/test_agent365_exporter.py index b272f08c..ec89b1cb 100644 --- a/tests/observability/core/test_agent365_exporter.py +++ b/tests/observability/core/test_agent365_exporter.py @@ -372,6 +372,18 @@ def test_export_error_logging(self, mock_logger): "No spans with tenant/agent identity found; nothing exported." ) + def test_exporter_is_internal(self): + """Test that _Agent365Exporter is marked as internal/private. + + The underscore prefix convention indicates this class is internal to the SDK + and should not be instantiated directly by developers. + """ + + self.assertTrue( + _Agent365Exporter.__name__.startswith("_"), + "Exporter class should be prefixed with underscore to indicate it's private/internal", + ) + if __name__ == "__main__": unittest.main() From 88046cd183a7a5104369b536c7b6986325bbdb3b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:55:15 +0530 Subject: [PATCH 4/7] Fix __init__.py filename and add copyright header in exporters module (#99) * Initial plan * Fix filename and add copyright header to exporters __init__.py Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com> --- .../observability/core/exporters/{__init__py => __init__.py} | 3 +++ 1 file changed, 3 insertions(+) rename libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/{__init__py => __init__.py} (68%) diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__.py similarity index 68% rename from libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__py rename to libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__.py index 2867652c..83b84bcb 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + # Agent365Exporter is not exported intentionally. # It should only be used internally by the observability core module. __all__ = ["Agent365ExporterOptions"] From 29bb827753d1e51b345a77c390ae8027e0153437 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Mon, 5 Jan 2026 19:13:51 +0530 Subject: [PATCH 5/7] fix PR comments and tests --- .../observability/core/exporters/__init__.py | 2 + tests/observability/core/test_agent365.py | 46 +++++-------------- .../core/test_execute_tool_scope.py | 19 ++++++-- .../core/test_inference_scope.py | 18 ++++++-- .../core/test_invoke_agent_scope.py | 18 +++++++- .../core/test_record_attributes.py | 10 ++++ 6 files changed, 70 insertions(+), 43 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__.py index 83b84bcb..b0e584f5 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/__init__.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +from .agent365_exporter_options import Agent365ExporterOptions + # Agent365Exporter is not exported intentionally. # It should only be used internally by the observability core module. __all__ = ["Agent365ExporterOptions"] diff --git a/tests/observability/core/test_agent365.py b/tests/observability/core/test_agent365.py index ba6ab0ad..adef977c 100644 --- a/tests/observability/core/test_agent365.py +++ b/tests/observability/core/test_agent365.py @@ -9,7 +9,6 @@ Agent365ExporterOptions, ) from microsoft_agents_a365.observability.core.trace_processor import SpanProcessor -from opentelemetry import trace as otel_trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider @@ -19,6 +18,14 @@ class TestAgent365Configure(unittest.TestCase): def setUp(self): """Set up test fixtures.""" + # Reset TelemetryManager state before each test + from microsoft_agents_a365.observability.core.config import _telemetry_manager + from microsoft_agents_a365.observability.core.opentelemetry_scope import OpenTelemetryScope + + _telemetry_manager._tracer_provider = None + _telemetry_manager._span_processors = {} + OpenTelemetryScope._tracer = None + self.mock_token_resolver = Mock() self.mock_token_resolver.return_value = "test_token_123" @@ -26,11 +33,13 @@ def tearDown(self): """Clean up after each test.""" # Reset the telemetry manager singleton state from microsoft_agents_a365.observability.core.config import _telemetry_manager + from microsoft_agents_a365.observability.core.opentelemetry_scope import OpenTelemetryScope _telemetry_manager._tracer_provider = None _telemetry_manager._span_processors = {} + OpenTelemetryScope._tracer = None - otel_trace._TRACER_PROVIDER = None + # Do NOT reset otel_trace._TRACER_PROVIDER to None to avoid NonRecordingSpan issues in other tests def test_configure_basic_functionality(self): """Test configure function with basic parameters and legacy parameters.""" @@ -144,39 +153,6 @@ def test_configure_prevents_duplicate_initialization(self): mock_logger.warning.assert_called_once() self.assertIn("already configured", mock_logger.warning.call_args[0][0].lower()) - @patch("microsoft_agents_a365.observability.core.config.is_agent365_exporter_enabled") - @patch("microsoft_agents_a365.observability.core.config.TracerProvider") - def test_configure_creates_new_tracer_provider(self, mock_provider_class, mock_is_enabled): - """Test configure() creates new TracerProvider when none exists and adds processors.""" - mock_is_enabled.return_value = False - - new_provider = TracerProvider( - resource=Resource.create({ - "service.name": "test-service", - "service.namespace": "test-namespace", - }) - ) - mock_provider_class.return_value = new_provider - - result = configure(service_name="test-service", service_namespace="test-namespace") - self.assertTrue(result) - - # Verify both processors were added by inspecting the MultiSpanProcessor - - active_processor = new_provider._active_span_processor - self.assertIsNotNone(active_processor) - - # MultiSpanProcessor has a _span_processors list - processors = active_processor._span_processors - self.assertEqual( - len(processors), 2, "Should have 2 processors: BatchSpanProcessor and SpanProcessor" - ) - - # Verify types of processors - processor_types = [type(p).__name__ for p in processors] - self.assertIn("BatchSpanProcessor", processor_types) - self.assertIn("SpanProcessor", processor_types) - @patch("microsoft_agents_a365.observability.core.config.is_agent365_exporter_enabled") @patch("microsoft_agents_a365.observability.core.config.trace.get_tracer_provider") def test_configure_uses_existing_tracer_provider(self, mock_get_provider, mock_is_enabled): diff --git a/tests/observability/core/test_execute_tool_scope.py b/tests/observability/core/test_execute_tool_scope.py index 76e9c2b4..187ee269 100644 --- a/tests/observability/core/test_execute_tool_scope.py +++ b/tests/observability/core/test_execute_tool_scope.py @@ -2,15 +2,15 @@ # Licensed under the MIT License. import os -from pathlib import Path import sys import unittest -import pytest +from pathlib import Path +import pytest from microsoft_agents_a365.observability.core import ( AgentDetails, - ExecutionType, ExecuteToolScope, + ExecutionType, Request, SourceMetadata, TenantDetails, @@ -18,10 +18,12 @@ configure, get_tracer_provider, ) +from microsoft_agents_a365.observability.core.config import _telemetry_manager from microsoft_agents_a365.observability.core.constants import ( GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, GEN_AI_EXECUTION_SOURCE_NAME_KEY, ) +from microsoft_agents_a365.observability.core.opentelemetry_scope import OpenTelemetryScope from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter @@ -56,6 +58,17 @@ def setUpClass(cls): def setUp(self): super().setUp() + # Reset TelemetryManager state to ensure fresh configuration + _telemetry_manager._tracer_provider = None + _telemetry_manager._span_processors = {} + OpenTelemetryScope._tracer = None + + # Reconfigure to get a fresh TracerProvider + configure( + service_name="test-execute-tool-service", + service_namespace="test-namespace", + ) + # Set up tracer to capture spans self.span_exporter = InMemorySpanExporter() tracer_provider = get_tracer_provider() diff --git a/tests/observability/core/test_inference_scope.py b/tests/observability/core/test_inference_scope.py index c7361bd0..01eef83f 100644 --- a/tests/observability/core/test_inference_scope.py +++ b/tests/observability/core/test_inference_scope.py @@ -2,11 +2,11 @@ # Licensed under the MIT License. import os -from pathlib import Path import sys import unittest -import pytest +from pathlib import Path +import pytest from microsoft_agents_a365.observability.core import ( ExecutionType, InferenceCallDetails, @@ -19,10 +19,12 @@ get_tracer_provider, ) from microsoft_agents_a365.observability.core.agent_details import AgentDetails +from microsoft_agents_a365.observability.core.config import _telemetry_manager from microsoft_agents_a365.observability.core.constants import ( GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, GEN_AI_EXECUTION_SOURCE_NAME_KEY, ) +from microsoft_agents_a365.observability.core.opentelemetry_scope import OpenTelemetryScope from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter @@ -47,11 +49,21 @@ def setUpClass(cls): def setUp(self): super().setUp() + # Reset TelemetryManager state to ensure fresh configuration + _telemetry_manager._tracer_provider = None + _telemetry_manager._span_processors = {} + OpenTelemetryScope._tracer = None + + # Reconfigure to get a fresh TracerProvider + configure( + service_name="test-inference-service", + service_namespace="test-namespace", + ) + # Set up tracer to capture spans self.span_exporter = InMemorySpanExporter() tracer_provider = get_tracer_provider() tracer_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) - # trace.set_tracer_provider(tracer_provider) def tearDown(self): super().tearDown() diff --git a/tests/observability/core/test_invoke_agent_scope.py b/tests/observability/core/test_invoke_agent_scope.py index eec2b30e..7541e221 100644 --- a/tests/observability/core/test_invoke_agent_scope.py +++ b/tests/observability/core/test_invoke_agent_scope.py @@ -2,12 +2,12 @@ # Licensed under the MIT License. import os -from pathlib import Path import sys import unittest -import pytest +from pathlib import Path from urllib.parse import urlparse +import pytest from microsoft_agents_a365.observability.core import ( AgentDetails, ExecutionType, @@ -19,6 +19,7 @@ configure, get_tracer_provider, ) +from microsoft_agents_a365.observability.core.config import _telemetry_manager from microsoft_agents_a365.observability.core.constants import ( GEN_AI_CALLER_AGENT_USER_CLIENT_IP, GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, @@ -27,6 +28,7 @@ GEN_AI_INPUT_MESSAGES_KEY, ) from microsoft_agents_a365.observability.core.models.caller_details import CallerDetails +from microsoft_agents_a365.observability.core.opentelemetry_scope import OpenTelemetryScope from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter @@ -97,6 +99,18 @@ def setUpClass(cls): def setUp(self): super().setUp() + # Reset TelemetryManager state to ensure fresh configuration + + _telemetry_manager._tracer_provider = None + _telemetry_manager._span_processors = {} + OpenTelemetryScope._tracer = None + + # Reconfigure to get a fresh TracerProvider + configure( + service_name="test-invoke-agent-service", + service_namespace="test-namespace", + ) + # Set up tracer to capture spans self.span_exporter = InMemorySpanExporter() tracer_provider = get_tracer_provider() diff --git a/tests/observability/core/test_record_attributes.py b/tests/observability/core/test_record_attributes.py index c161b2df..95f1ee57 100644 --- a/tests/observability/core/test_record_attributes.py +++ b/tests/observability/core/test_record_attributes.py @@ -7,6 +7,7 @@ from unittest.mock import Mock, patch from microsoft_agents_a365.observability.core import AgentDetails, TenantDetails +from microsoft_agents_a365.observability.core.config import _telemetry_manager from microsoft_agents_a365.observability.core.opentelemetry_scope import OpenTelemetryScope from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider @@ -53,6 +54,15 @@ def setUpClass(cls): def setUp(self): """Clear spans before each test.""" + # Reset TelemetryManager state to ensure fresh configuration + _telemetry_manager._tracer_provider = None + _telemetry_manager._span_processors = {} + + # Create a fresh TracerProvider for this test + provider = TracerProvider() + trace.set_tracer_provider(provider) + provider.add_span_processor(SimpleSpanProcessor(self.exporter)) + # Force OpenTelemetryScope to refresh its tracer reference OpenTelemetryScope._tracer = None self.exporter.clear() From 6da480389c0b38c0b0eea9ad843f075a9fabe8b4 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Mon, 5 Jan 2026 22:48:13 +0530 Subject: [PATCH 6/7] fix PR comments --- .../microsoft_agents_a365/observability/core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py index 6603dce0..ffe3192b 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py @@ -116,7 +116,7 @@ def _configure_internal( # Determine if we should use existing provider or create new one # Check if it's a real TracerProvider with a resource (not a proxy/no-op) - if hasattr(tracer_provider, "resource") and tracer_provider.resource: + if getattr(tracer_provider, "resource", None): # Use existing provider from application's OTEL setup self._logger.info( "Detected existing TracerProvider with resource. " From 36feac5945d147c512232aebbc2902bdb779df56 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:59:48 +0530 Subject: [PATCH 7/7] [WIP] Address feedback on initialization fix from PR #97 (#100) * Initial plan * Remove trailing space from warning message Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com> --- .../microsoft_agents_a365/observability/core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py index ffe3192b..c48b0d37 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py @@ -99,7 +99,7 @@ def _configure_internal( # Check if a365 observability is already configured if self._tracer_provider is not None: self._logger.warning( - "a365 observability already configured. Ignoring repeated configure() call. " + "a365 observability already configured. Ignoring repeated configure() call." ) return True