From 709002b7efa1f42c7f1a673a7fcfb9ccb68021dd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 20:38:35 +0000 Subject: [PATCH 1/2] fix: Apply log level filtering to BufferHandler to suppress debug messages BufferHandler extends logging.Handler directly, not logging.StreamHandler. The isinstance(h, logging.StreamHandler) check skipped it, leaving its level at NOTSET (0) which passes all messages through. This caused debug messages (e.g. status query responses from SyncUnixClient) to accumulate in the buffer and appear in the REST API /api/runtime-logs endpoint even when --print-debug was not set. https://claude.ai/code/session_01JAxvToztqockUCUQSC7dv7 --- webserver/logger/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/webserver/logger/__init__.py b/webserver/logger/__init__.py index 1469219b..ce43d45e 100644 --- a/webserver/logger/__init__.py +++ b/webserver/logger/__init__.py @@ -49,7 +49,6 @@ def get_logger(name="runtime", use_buffer: bool = False): # Always update all handler levels to reflect current config for h in logger.handlers: - if isinstance(h, logging.StreamHandler): - h.setLevel(effective_level) + h.setLevel(effective_level) return logger, shared_buffer_handler From adb5b51cff91bb52d301243dcc2ee316ffdf4f3d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 20:57:47 +0000 Subject: [PATCH 2/2] fix: Normalize WARN level from C runtime and remove dead logger module Three logging fixes: 1. LogParser now maps C runtime's "WARN" to Python's "WARNING" for both the numeric level (used by handler filtering) and the string stored in log entries. Previously WARN messages got level=INFO (wrong numeric mapping) and were stored as "WARN" in the buffer while Python-originated warnings stored "WARNING", breaking level filtering in the REST API. 2. Remove dead webserver/logger/logger.py which defined a shadowed get_logger() that created separate BufferHandlers without level filtering. The __init__.py import that pulled it in was also removed. 3. Update test_log_levels_stored to verify debug messages are filtered by default, and add test_log_levels_stored_with_print_debug to verify they pass through when print_debug is enabled. https://claude.ai/code/session_01JAxvToztqockUCUQSC7dv7 --- tests/pytest/logger/test_logger_buffer.py | 26 ++++++++++++++++++++- webserver/logger/__init__.py | 1 - webserver/logger/logger.py | 28 ----------------------- webserver/logger/parser.py | 9 ++++++++ 4 files changed, 34 insertions(+), 30 deletions(-) delete mode 100644 webserver/logger/logger.py diff --git a/tests/pytest/logger/test_logger_buffer.py b/tests/pytest/logger/test_logger_buffer.py index 20455076..4b159501 100644 --- a/tests/pytest/logger/test_logger_buffer.py +++ b/tests/pytest/logger/test_logger_buffer.py @@ -27,6 +27,7 @@ def test_multiple_logs_stored(test_logger): assert msg in logs[i]["message"] def test_log_levels_stored(test_logger): + """Debug messages are filtered when print_debug is False (default).""" logger, buffer = test_logger logger.debug("Debug message") logger.info("Info message") @@ -34,7 +35,30 @@ def test_log_levels_stored(test_logger): logger.error("Error message") logs = buffer.get_logs() levels = [log["level"] for log in logs] - assert levels == ["DEBUG", "INFO", "WARNING", "ERROR"] + assert levels == ["INFO", "WARNING", "ERROR"] + + +def test_log_levels_stored_with_print_debug(test_logger): + """Debug messages pass through when print_debug is enabled.""" + from webserver.logger.config import LoggerConfig + from webserver.logger import get_logger + + LoggerConfig.print_debug = True + try: + # Re-fetch logger to apply new level to handlers + logger, buffer = get_logger("test_logger", use_buffer=True) + buffer.clear() + logger.debug("Debug message") + logger.info("Info message") + logger.warning("Warning message") + logger.error("Error message") + logs = buffer.get_logs() + levels = [log["level"] for log in logs] + assert levels == ["DEBUG", "INFO", "WARNING", "ERROR"] + finally: + LoggerConfig.print_debug = False + # Re-fetch to restore INFO level on handlers + get_logger("test_logger", use_buffer=True) def test_normalize_no_microseconds(test_logger): logger, buffer = test_logger diff --git a/webserver/logger/__init__.py b/webserver/logger/__init__.py index ce43d45e..25704e14 100644 --- a/webserver/logger/__init__.py +++ b/webserver/logger/__init__.py @@ -2,7 +2,6 @@ import logging import sys -from .logger import get_logger from .parser import LogParser from .bufferhandler import BufferHandler from .formatter import JsonFormatter, HumanReadableFormatter diff --git a/webserver/logger/logger.py b/webserver/logger/logger.py deleted file mode 100644 index 943cbd77..00000000 --- a/webserver/logger/logger.py +++ /dev/null @@ -1,28 +0,0 @@ -# logger/logger.py -import logging -import sys -from .formatter import JsonFormatter, HumanReadableFormatter -from .bufferhandler import BufferHandler - - -def get_logger(name: str = "logger", - level: int = logging.INFO, - use_buffer: bool = False): - """Return a logger instance with custom formatting.""" - - collector_logger = logging.getLogger(name) - collector_logger.setLevel(logging.DEBUG) - - # Always ensure a StreamHandler exists - if not any(isinstance(h, logging.StreamHandler) for h in collector_logger.handlers): - stream_handler = logging.StreamHandler(sys.stdout) - stream_handler.setFormatter(HumanReadableFormatter()) - collector_logger.addHandler(stream_handler) - - buffer_handler = None - if use_buffer and not any(isinstance(h, BufferHandler) for h in collector_logger.handlers): - buffer_handler = BufferHandler() - buffer_handler.setFormatter(JsonFormatter()) - collector_logger.addHandler(buffer_handler) - - return collector_logger, buffer_handler diff --git a/webserver/logger/parser.py b/webserver/logger/parser.py index 05bcda62..c0b7195b 100644 --- a/webserver/logger/parser.py +++ b/webserver/logger/parser.py @@ -9,11 +9,17 @@ LEVEL_MAP = { "DEBUG": logging.DEBUG, "INFO": logging.INFO, + "WARN": logging.WARNING, "WARNING": logging.WARNING, "ERROR": logging.ERROR, "CRITICAL": logging.CRITICAL, } +# Normalize non-standard level names to Python conventions +LEVEL_NORMALIZE = { + "WARN": "WARNING", +} + class LogParser: def __init__(self, collector_logger: logging.Logger): @@ -37,7 +43,9 @@ def parse_and_log(self, line: str): # Preserve incoming JSON fields, but ensure timestamp is present parsed.setdefault("timestamp", str(timestamp)) level_name = parsed.get("level", "INFO") + level_name = LEVEL_NORMALIZE.get(level_name, level_name) level = LEVEL_MAP.get(level_name, logging.INFO) + parsed["level"] = level_name log_entry = parsed else: raise ValueError("Not a valid log JSON dict") @@ -46,6 +54,7 @@ def parse_and_log(self, line: str): match = LOG_PATTERN.match(sline) if match: level_name = match["level"] + level_name = LEVEL_NORMALIZE.get(level_name, level_name) level = LEVEL_MAP.get(level_name, logging.INFO) message = match["message"] else: