diff --git a/src/powercontext/server/logging.py b/src/powercontext/server/logging.py index 36f569c64..10e13c6ce 100644 --- a/src/powercontext/server/logging.py +++ b/src/powercontext/server/logging.py @@ -91,6 +91,23 @@ def filter(self, record: logging.LogRecord) -> bool: return True +class _UvicornDisplayNameFilter(logging.Filter): + """Rewrite uvicorn's logger name for downstream display. + + Uvicorn emits lifecycle and error records through ``uvicorn.error``. + Logger selection, hierarchy, and effective-level checks use that logger. + This filter then rewrites only ``record.name``, so later filters on the + same logger and downstream handler filters and formatters observe + ``uvicorn``. Parent logger filters are not applied during propagation. + """ + + @override + def filter(self, record: logging.LogRecord) -> bool: + if record.name == "uvicorn.error": + record.name = "uvicorn" + return True + + def configure_server_logging(config: ServerLoggingConfig) -> None: """Configure process logging for the foreground Server command.""" @@ -106,7 +123,10 @@ def configure_server_logging(config: ServerLoggingConfig) -> None: logging.config.dictConfig({ "version": 1, "disable_existing_loggers": False, - "filters": {"operational": {"()": _HumanContextFilter}}, + "filters": { + "operational": {"()": _HumanContextFilter}, + "uvicorn_display_name": {"()": _UvicornDisplayNameFilter}, + }, "formatters": {"server": formatter}, "handlers": { "server": { @@ -126,6 +146,7 @@ def configure_server_logging(config: ServerLoggingConfig) -> None: "handlers": ["server"], "level": config.level, "propagate": False, + "filters": ["uvicorn_display_name"], }, }, }) diff --git a/tests/test_server_logging.py b/tests/test_server_logging.py index 657202729..33f8432c6 100644 --- a/tests/test_server_logging.py +++ b/tests/test_server_logging.py @@ -16,6 +16,8 @@ import json import logging +import subprocess +import sys from fastapi.testclient import TestClient @@ -25,6 +27,50 @@ from powercontext.server.settings import McpConfig, ServerLoggingConfig, ServerSettings +def _run_configured_logging_probe(log_format: str) -> list[str]: + script = f""" +import logging + +from powercontext.server.logging import configure_server_logging +from powercontext.server.settings import ServerLoggingConfig + +configure_server_logging(ServerLoggingConfig(format={log_format!r})) +logging.getLogger("uvicorn.error").info("Started server process") +logging.getLogger("uvicorn.error").error("Server startup failed") +logging.getLogger("powercontext.server.factory").info("PowerContext Server is ready") +""" + result = subprocess.run( + [sys.executable, "-c", script], + check=True, + capture_output=True, + text=True, + timeout=120, + ) + return result.stdout.splitlines() + + +def test_configured_console_logging_uses_uvicorn_display_name() -> None: + lines = _run_configured_logging_probe("console") + + assert len(lines) == 3 + messages = [line.split(" ", 1)[1] for line in lines] + assert messages == [ + "INFO uvicorn Started server process", + "ERROR uvicorn Server startup failed", + "INFO powercontext.server.factory PowerContext Server is ready", + ] + + +def test_configured_json_logging_uses_uvicorn_display_name() -> None: + payloads = [json.loads(line) for line in _run_configured_logging_probe("json")] + + assert [(payload["level"], payload["logger"], payload["message"]) for payload in payloads] == [ + ("INFO", "uvicorn", "Started server process"), + ("ERROR", "uvicorn", "Server startup failed"), + ("INFO", "powercontext.server.factory", "PowerContext Server is ready"), + ] + + def test_json_formatter_emits_stable_operational_fields() -> None: record = logging.makeLogRecord({ "name": "powercontext.server.access",