fix(logging): preserve level across uvicorn startup

This commit is contained in:
RaresKeY 2026-07-18 13:08:29 +00:00
parent 71ea62f934
commit 1a5df9edc0
5 changed files with 106 additions and 6 deletions

9
app.py
View file

@ -88,6 +88,7 @@ from core.log_safety import (
ScopedDiagnosticsFilter,
application_log_settings,
configure_uvicorn_log_levels,
uvicorn_log_config,
)
_root_logger = logging.getLogger()
@ -1298,4 +1299,10 @@ if __name__ == "__main__":
bind_host = os.getenv("APP_BIND", "127.0.0.1")
bind_port = int(os.getenv("APP_PORT", "7000"))
uvicorn.run(app, host=bind_host, port=bind_port, log_level=_application_log_level)
uvicorn.run(
app,
host=bind_host,
port=bind_port,
log_level=_application_log_level,
log_config=uvicorn_log_config(_application_log_level),
)

View file

@ -9,13 +9,18 @@ also doubles as a sanitizer barrier for CodeQL's clear-text-logging query.
from __future__ import annotations
from copy import deepcopy
import logging
from urllib.parse import urlparse, urlunparse
CAPABILITY_DIAGNOSTICS_LOGGER = "src.model_capability_readers"
UVICORN_LOGGER_NAMES = ("uvicorn", "uvicorn.error", "uvicorn.access")
UVICORN_LOGGER_NAMES = (
"uvicorn",
"uvicorn.error",
"uvicorn.access",
"uvicorn.asgi",
)
_LOG_LEVELS = {
"DEBUG": logging.DEBUG,
@ -53,6 +58,18 @@ def configure_uvicorn_log_levels(application_level: int) -> None:
logging.getLogger(logger_name).setLevel(application_level)
def uvicorn_log_config(application_level: int) -> dict:
"""Return a Uvicorn config that preserves the mapped level on direct runs."""
from uvicorn.config import LOGGING_CONFIG
config = deepcopy(LOGGING_CONFIG)
loggers = config.setdefault("loggers", {})
for logger_name in UVICORN_LOGGER_NAMES:
loggers.setdefault(logger_name, {})["level"] = application_level
return config
class ScopedDiagnosticsFilter(logging.Filter):
"""Allow normal application records plus one explicitly scoped DEBUG log."""

View file

@ -128,9 +128,13 @@ if __name__ == "__main__":
import uvicorn
# Import the FastAPI app from app.py
from app import app
from core.log_safety import application_log_settings, uvicorn_log_config
bind_host = os.getenv("APP_BIND", "127.0.0.1")
bind_port = int(os.getenv("APP_PORT", "7000"))
application_log_level, _ = application_log_settings(
os.getenv("LOG_LEVEL", "INFO")
)
url = f"http://{bind_host}:{bind_port}"
if getattr(sys, 'frozen', False):
@ -139,4 +143,10 @@ if __name__ == "__main__":
# Start system tray manager thread
threading.Thread(target=setup_system_tray, args=(url,), daemon=True).start()
uvicorn.run(app, host=bind_host, port=bind_port, log_level="info")
uvicorn.run(
app,
host=bind_host,
port=bind_port,
log_level=application_log_level,
log_config=uvicorn_log_config(application_log_level),
)

View file

@ -1,13 +1,18 @@
import logging
import subprocess
import sys
from textwrap import dedent
import pytest
from core.log_safety import (
CAPABILITY_DIAGNOSTICS_LOGGER,
ScopedDiagnosticsFilter,
UVICORN_LOGGER_NAMES,
application_log_settings,
configure_uvicorn_log_levels,
redact_url,
uvicorn_log_config,
)
@ -67,7 +72,7 @@ def test_application_log_settings_scope_debug_and_fail_closed(
def test_configure_uvicorn_log_levels_clamps_non_propagating_loggers():
logger_names = ("uvicorn", "uvicorn.error", "uvicorn.access")
logger_names = UVICORN_LOGGER_NAMES
previous_levels = {
name: logging.getLogger(name).level for name in logger_names
}
@ -85,6 +90,61 @@ def test_configure_uvicorn_log_levels_clamps_non_propagating_loggers():
logging.getLogger(name).setLevel(level)
def test_uvicorn_log_config_sets_all_named_loggers_without_mutating_default():
from uvicorn.config import LOGGING_CONFIG
configured = uvicorn_log_config(logging.ERROR)
assert all(
configured["loggers"][name]["level"] == logging.ERROR
for name in UVICORN_LOGGER_NAMES
)
assert LOGGING_CONFIG["loggers"]["uvicorn"]["level"] == "INFO"
def test_uvicorn_levels_hold_across_external_and_direct_config_order():
result = subprocess.run(
[
sys.executable,
"-c",
dedent(
"""
import logging
import uvicorn
from core.log_safety import (
UVICORN_LOGGER_NAMES,
configure_uvicorn_log_levels,
uvicorn_log_config,
)
uvicorn.Config("app:app", log_level="debug")
configure_uvicorn_log_levels(logging.INFO)
assert all(
logging.getLogger(name).level == logging.INFO
for name in UVICORN_LOGGER_NAMES
)
uvicorn.Config(
"app:app",
log_level=logging.ERROR,
log_config=uvicorn_log_config(logging.ERROR),
)
assert all(
logging.getLogger(name).level == logging.ERROR
for name in UVICORN_LOGGER_NAMES
)
"""
),
],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr
def _record(name: str, level: int) -> logging.LogRecord:
return logging.LogRecord(name, level, __file__, 1, "message", (), None)

View file

@ -57,7 +57,9 @@ def test_fallback_debug_log_is_explicit_and_has_no_capability_claims(caplog):
def test_web_app_logging_uses_existing_log_level_environment_toggle():
source = (Path(__file__).resolve().parents[1] / "app.py").read_text(encoding="utf-8")
root = Path(__file__).resolve().parents[1]
source = (root / "app.py").read_text(encoding="utf-8")
launcher_source = (root / "launcher.py").read_text(encoding="utf-8")
assert 'os.getenv("LOG_LEVEL", "INFO")' in source
assert "application_log_settings(_log_level_name)" in source
@ -66,3 +68,7 @@ def test_web_app_logging_uses_existing_log_level_environment_toggle():
assert "_console_h.addFilter(_diagnostics_filter)" in source
assert "_file_h.addFilter(_diagnostics_filter)" in source
assert "log_level=_application_log_level" in source
assert "log_config=uvicorn_log_config(_application_log_level)" in source
assert "application_log_settings(" in launcher_source
assert "log_level=application_log_level" in launcher_source
assert "log_config=uvicorn_log_config(application_log_level)" in launcher_source