Skip to content
Open
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
31 changes: 29 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,35 @@ def register_static_mime_types() -> None:
# ========= LOGGING =========
import logging.handlers
from core.constants import DATA_DIR
from core.log_safety import (
CAPABILITY_DIAGNOSTICS_LOGGER,
ScopedDiagnosticsFilter,
application_log_settings,
configure_uvicorn_log_levels,
uvicorn_log_config,
)

_root_logger = logging.getLogger()
_root_logger.setLevel(logging.INFO)
_log_level_name = os.getenv("LOG_LEVEL", "INFO").strip().upper()
_application_log_level, _capability_debug = application_log_settings(_log_level_name)
_root_logger.setLevel(_application_log_level)
configure_uvicorn_log_levels(_application_log_level)
logging.getLogger(CAPABILITY_DIAGNOSTICS_LOGGER).setLevel(
logging.DEBUG if _capability_debug else logging.NOTSET
)
_diagnostics_filter = ScopedDiagnosticsFilter(
_application_log_level,
capability_debug=_capability_debug,
)
_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# Clear existing handlers to avoid duplicates
for _h in list(_root_logger.handlers):
_root_logger.removeHandler(_h)

_console_h = logging.StreamHandler()
_console_h.setLevel(logging.DEBUG)
_console_h.addFilter(_diagnostics_filter)
_console_h.setFormatter(_formatter)
_root_logger.addHandler(_console_h)

Expand All @@ -107,6 +126,8 @@ def register_static_mime_types() -> None:
_file_h = logging.handlers.RotatingFileHandler(
_log_file, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8"
)
_file_h.setLevel(logging.DEBUG)
_file_h.addFilter(_diagnostics_filter)
_file_h.setFormatter(_formatter)
_root_logger.addHandler(_file_h)
except Exception as e:
Expand Down Expand Up @@ -1278,4 +1299,10 @@ async def _shutdown_event():
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="info")
uvicorn.run(
app,
host=bind_host,
port=bind_port,
log_level=_application_log_level,
log_config=uvicorn_log_config(_application_log_level),
)
83 changes: 83 additions & 0 deletions core/log_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,92 @@
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.asgi",
)

_LOG_LEVELS = {
"DEBUG": logging.DEBUG,
"INFO": logging.INFO,
"WARN": logging.WARNING,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
"FATAL": logging.CRITICAL,
"CRITICAL": logging.CRITICAL,
}


def application_log_settings(value: object) -> tuple[int, bool]:
"""Return the safe app level and whether scoped capability debug is on.

Application-wide DEBUG logging can expose request bodies, provider
responses, or credentials from unrelated libraries. The model capability
catalog has a deliberately bounded DEBUG summary, so a DEBUG request is
translated into INFO for the application and enabled only for that logger.
Unknown values also fail closed to INFO.
"""

requested = _LOG_LEVELS.get(str(value or "INFO").strip().upper(), logging.INFO)
return max(requested, logging.INFO), requested == logging.DEBUG


def configure_uvicorn_log_levels(application_level: int) -> None:
"""Apply the mapped app level to Uvicorn's non-propagating loggers.

External entrypoints configure these loggers before importing ``app`` and
otherwise bypass the root logger's level and scoped diagnostics filter.
"""

for logger_name in UVICORN_LOGGER_NAMES:
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."""

def __init__(
self,
application_level: int,
*,
capability_debug: bool = False,
) -> None:
super().__init__()
self.application_level = application_level
self.capability_debug = capability_debug

def filter(self, record: logging.LogRecord) -> bool:
if record.levelno >= self.application_level:
return True
return (
self.capability_debug
and record.levelno >= logging.DEBUG
and record.name == CAPABILITY_DIAGNOSTICS_LOGGER
)


def redact_url(url: str) -> str:
"""Return a URL safe for logs by removing userinfo and query/fragment.

Expand Down
12 changes: 11 additions & 1 deletion launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,13 @@ def open_browser(url):
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):
Expand All @@ -139,4 +143,10 @@ def open_browser(url):
# 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),
)
Loading