valhalla / backend /src /core /log.py
Dontcryx_07
terminal
98f0a17
Raw
History Blame Contribute Delete
4.73 kB
"""log — centralized logging setup for the whole project.
Every module imports get_logger(__name__) here so format, destinations,
and level stay consistent across the engine, planner, and server.
Architecture: a dependency of every backend module; no business logic.
Design: logs land in backend/output/logs with timestamps for post-hoc
analysis of long runs. Console output is off by default — the admin log
terminal (src/core/log_relay.py) mirrors every line to the live log file
and the admin-only /api/logs endpoints instead.
"""
from __future__ import annotations
import logging
import logging.config
from datetime import datetime
from pathlib import Path
from typing import Optional
# Path config lives in one place: src/config.py
from src.config import LOG_DIR, LOG_LEVEL as _DEFAULT_LEVEL_NAME
DEFAULT_LEVEL = getattr(logging, _DEFAULT_LEVEL_NAME.upper(), logging.INFO)
LOG_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
_configured = False
def _build_log_path(run_id: Optional[str] = None) -> Path:
LOG_DIR.mkdir(parents=True, exist_ok=True)
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{stamp}_{run_id}.log" if run_id else f"{stamp}.log"
return LOG_DIR / filename
def setup_logging(
level: int = DEFAULT_LEVEL,
run_id: Optional[str] = None,
console: bool = False,
file: bool = True,
max_bytes: int = 5 * 1024 * 1024,
backup_count: int = 5,
) -> Optional[Path]:
"""
Configures the ROOT logger once. Idempotent -- a second call is a no-op,
so accidentally calling it from more than one entrypoint (e.g. both a
test script and an imported module) will never duplicate handlers or
duplicate every log line.
level -- root log level (module-specific loggers can still be
set louder/quieter individually if you ever need that)
run_id -- optional tag folded into the log filename, e.g. a
persona name or simulation id, so a run's logs are
easy to find in output/logs/
console -- also stream logs to stdout (default off; watch the
admin log terminal instead)
file -- also write logs to output/logs/<timestamp>[_<run_id>].log
max_bytes /
backup_count -- rotation settings for the file handler
Returns the Path of the log file created, or None if file=False.
"""
global _configured
if _configured:
return None
handlers: dict = {}
log_path: Optional[Path] = None
if console:
handlers["console"] = {
"class": "logging.StreamHandler",
"formatter": "standard",
"stream": "ext://sys.stdout",
"level": level,
}
if file:
log_path = _build_log_path(run_id)
handlers["file"] = {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "standard",
"filename": str(log_path),
"maxBytes": max_bytes,
"backupCount": backup_count,
"encoding": "utf-8",
"level": level,
}
config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": LOG_FORMAT,
"datefmt": DATE_FORMAT,
},
},
"handlers": handlers,
"root": {
"level": level,
"handlers": list(handlers.keys()),
},
# Quiet down noisy third-party loggers so your own log lines don't
# get buried -- add to this as you bring in more SDKs.
"loggers": {
"google_genai": {"level": "WARNING", "propagate": True},
"httpx": {"level": "WARNING", "propagate": True},
"httpcore": {"level": "WARNING", "propagate": True},
},
}
logging.config.dictConfig(config)
_configured = True
# Mirror every emitted line into the admin log terminal's buffer and the
# live log file, regardless of the console/file toggles above.
try:
from src.core.log_relay import install_relay
install_relay()
except Exception:
pass
if log_path:
logging.getLogger(__name__).info("[log] logging initialized -> %s", log_path)
return log_path
def get_logger(name: str) -> logging.Logger:
"""
Thin wrapper so call sites read `from src.core.log import get_logger`
instead of raw `logging.getLogger`. Lazily calls setup_logging() with
defaults if nothing has configured logging yet.
"""
if not _configured:
setup_logging()
return logging.getLogger(name)