Files
eryao/backend/src/core/logging/config.py
T
2026-03-31 13:32:22 +08:00

112 lines
3.6 KiB
Python

from __future__ import annotations
import logging
from logging.config import dictConfig
from pathlib import Path
from typing import cast
import structlog
from core.config.settings import PROJECT_ROOT, RuntimeSettings, Settings, config
from core.logging.formatters import (
build_plain_formatter,
build_processor_formatter,
ensure_message_key,
)
from core.logging.filters import build_sensitive_data_processor
from core.logging.handlers import build_file_handler_config
def _ensure_log_dirs(runtime: RuntimeSettings) -> None:
_resolve_log_path(runtime.log_dir).mkdir(parents=True, exist_ok=True)
_resolve_log_path(runtime.log_error_dir).mkdir(parents=True, exist_ok=True)
def _resolve_log_path(path: str) -> Path:
candidate = Path(path)
if candidate.is_absolute():
return candidate
return PROJECT_ROOT / candidate
def build_logging_config(runtime: RuntimeSettings) -> dict[str, object]:
log_dir = _resolve_log_path(runtime.log_dir)
error_dir = _resolve_log_path(runtime.log_error_dir)
formatter_name = "json" if runtime.log_json else "plain"
file_handler = build_file_handler_config(
runtime,
file_path=log_dir / runtime.log_file_name,
level=runtime.log_level,
formatter=formatter_name,
)
error_handler = build_file_handler_config(
runtime,
file_path=error_dir / runtime.log_error_file_name,
level="ERROR",
formatter=formatter_name,
filters=["error_only"],
)
return {
"version": 1,
"disable_existing_loggers": False,
"filters": {
"error_only": {
"()": "core.logging.filters.ErrorLevelFilter",
}
},
"formatters": {
"json": {
"()": build_processor_formatter,
"sensitive_fields": runtime.log_sensitive_fields,
},
"plain": {
"()": build_plain_formatter,
"sensitive_fields": runtime.log_sensitive_fields,
},
},
"handlers": {
"file": file_handler,
"error": error_handler,
},
"root": {
"handlers": ["file", "error"],
"level": runtime.log_level,
},
}
def configure_logging(settings: Settings | None = None) -> None:
active_settings = settings if settings is not None else cast(Settings, config)
runtime = active_settings.runtime
try:
_ensure_log_dirs(runtime)
dictConfig(build_logging_config(runtime))
except (OSError, ValueError) as exc:
logging.basicConfig(level=runtime.log_level)
logging.getLogger(__name__).error("Logging setup failed", exc_info=exc)
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
structlog.processors.CallsiteParameterAdder(
parameters=[
structlog.processors.CallsiteParameter.MODULE,
structlog.processors.CallsiteParameter.FUNC_NAME,
structlog.processors.CallsiteParameter.LINENO,
]
),
build_sensitive_data_processor(runtime.log_sensitive_fields),
ensure_message_key,
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)