Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/powercontext/server/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand All @@ -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": {
Expand All @@ -126,6 +146,7 @@ def configure_server_logging(config: ServerLoggingConfig) -> None:
"handlers": ["server"],
"level": config.level,
"propagate": False,
"filters": ["uvicorn_display_name"],
},
},
})
Expand Down
46 changes: 46 additions & 0 deletions tests/test_server_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import json
import logging
import subprocess
import sys

from fastapi.testclient import TestClient

Expand All @@ -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",
Expand Down
Loading