diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index 9fec610e24..0130cc30f3 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -1355,9 +1355,10 @@ def _configure_providers( logger_provider = LoggerProvider(resource=resource) for log_exporter in log_exporters: logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter)) - # Attach a handler with the provider to the root logger - handler = LoggingHandler(logger_provider=logger_provider) - logger.addHandler(handler) + # Attach a handler with the provider to the agent_framework logger, unless the root logger + # already has an OTel handler: records propagate to it and would otherwise be exported twice. + if not any(isinstance(h, LoggingHandler) for h in logging.getLogger().handlers): + logger.addHandler(LoggingHandler(logger_provider=logger_provider)) set_logger_provider(logger_provider) # metrics diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index fab710a7c5..fbcbd35511 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -7727,3 +7727,37 @@ async def test_chat_capture_messages_called_when_span_recording( assert mock_capture_v1_36.call_count == 2 assert mock_capture_experimental.call_count == 2 assert mock_capture_response.call_count == 1 + + +def _otel_log_handlers(target: logging.Logger) -> list[Any]: + from opentelemetry.sdk._logs import LoggingHandler + + return [h for h in target.handlers if isinstance(h, LoggingHandler)] + + +@pytest.mark.parametrize("root_handler_first", [False, True]) +def test_configure_providers_log_handler_not_duplicated_with_root_handler(root_handler_first: bool): + """MAF records are exported once when the app already has an OTel handler on the root logger.""" + from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler + from opentelemetry.sdk._logs.export import InMemoryLogRecordExporter + + from agent_framework.observability import ObservabilitySettings + + exporter = InMemoryLogRecordExporter() + root = logging.getLogger() + maf_logger = logging.getLogger("agent_framework") + root_handler = LoggingHandler(logger_provider=LoggerProvider()) + original_root_handlers = list(root.handlers) + original_maf_handlers = list(maf_logger.handlers) + handlers_before = len(_otel_log_handlers(maf_logger)) + try: + if root_handler_first: + root.addHandler(root_handler) + with patch("opentelemetry._logs.set_logger_provider"): + ObservabilitySettings(enable_instrumentation=True)._configure_providers([exporter]) # type: ignore[reportPrivateUsage] + + added = len(_otel_log_handlers(maf_logger)) - handlers_before + assert added == (0 if root_handler_first else 1) + finally: + root.handlers[:] = original_root_handlers + maf_logger.handlers[:] = original_maf_handlers