From 7d40f53218781e0e1831730c88d50b9c7ab1e974 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Thu, 17 Sep 2026 13:59:46 -0500 Subject: [PATCH] fix: avoid duplicate OTel handlers in setup_logging_bridge setup_logging_bridge added a new LoggingHandler on every call, so calling it more than once for the same logger produced duplicate OTel log records. Skip adding a handler when the target logger already has an OTel LoggingHandler. Testing: full suite (pytest -q) -> 400 passed. Red-green verified: tests/test_logging_bridge.py::test_setup_logging_bridge_is_idempotent fails on unpatched code and passes with the fix. Signed-off-by: Andrew White --- src/nemo/lens/logging_bridge.py | 6 +++++- tests/test_logging_bridge.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/nemo/lens/logging_bridge.py b/src/nemo/lens/logging_bridge.py index 4840de0..c8d964c 100644 --- a/src/nemo/lens/logging_bridge.py +++ b/src/nemo/lens/logging_bridge.py @@ -42,9 +42,13 @@ def setup_logging_bridge(logger_name: str = "", level: int = logging.INFO) -> No if not isinstance(logger_provider, LoggerProvider): return + logger = logging.getLogger(logger_name) + if any(isinstance(h, LoggingHandler) for h in logger.handlers): + return + handler = LoggingHandler(logger_provider=logger_provider) handler.setLevel(level) - logging.getLogger(logger_name).addHandler(handler) + logger.addHandler(handler) except ImportError: # OTel logs SDK not installed; silently skip pass diff --git a/tests/test_logging_bridge.py b/tests/test_logging_bridge.py index c0e2a34..792cf56 100644 --- a/tests/test_logging_bridge.py +++ b/tests/test_logging_bridge.py @@ -54,6 +54,36 @@ def __init__(self, logger_provider): logger.handlers.clear() +def test_setup_logging_bridge_is_idempotent(monkeypatch): + """Verify repeated setup does not stack duplicate OTel handlers.""" + logs_module = types.ModuleType("opentelemetry._logs") + sdk_logs_module = types.ModuleType("opentelemetry.sdk._logs") + + class LoggerProvider: + pass + + class LoggingHandler(logging.Handler): + def __init__(self, logger_provider): + super().__init__() + self.logger_provider = logger_provider + + provider = LoggerProvider() + logs_module.get_logger_provider = lambda: provider + sdk_logs_module.LoggerProvider = LoggerProvider + sdk_logs_module.LoggingHandler = LoggingHandler + monkeypatch.setitem(sys.modules, logs_module.__name__, logs_module) + monkeypatch.setitem(sys.modules, sdk_logs_module.__name__, sdk_logs_module) + + logger = logging.getLogger("nemo.lens.test.logging_bridge.idempotent") + logger.handlers.clear() + + setup_logging_bridge(logger.name) + setup_logging_bridge(logger.name) + + assert len(logger.handlers) == 1 + logger.handlers.clear() + + def test_setup_logging_bridge_skips_non_sdk_provider(monkeypatch): """Verify non-SDK logger providers are ignored without adding handlers.""" logs_module = types.ModuleType("opentelemetry._logs")