40 lines
1.5 KiB
Python
40 lines
1.5 KiB
Python
import logging
|
|
from logging.handlers import RotatingFileHandler
|
|
from pathlib import Path
|
|
|
|
|
|
def configure_logging(log_file: Path) -> None:
|
|
"""Configure console and file logging once for local service runs."""
|
|
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
root_logger = logging.getLogger()
|
|
root_logger.setLevel(logging.INFO)
|
|
|
|
if not _has_handler("signalscout-console"):
|
|
# Console logs are for the terminal window that runs `make run`.
|
|
console_handler = logging.StreamHandler()
|
|
console_handler.name = "signalscout-console"
|
|
console_handler.setFormatter(_formatter())
|
|
root_logger.addHandler(console_handler)
|
|
|
|
if not _has_handler("signalscout-file"):
|
|
# File logs persist agent failures after the terminal scrollback is gone.
|
|
file_handler = RotatingFileHandler(
|
|
log_file,
|
|
maxBytes=2_000_000,
|
|
backupCount=3,
|
|
encoding="utf-8",
|
|
)
|
|
file_handler.name = "signalscout-file"
|
|
file_handler.setFormatter(_formatter())
|
|
root_logger.addHandler(file_handler)
|
|
|
|
|
|
def _formatter() -> logging.Formatter:
|
|
# Include module name so source, API, and runner failures are easy to separate.
|
|
return logging.Formatter("%(asctime)s %(levelname)s [%(name)s] %(message)s")
|
|
|
|
|
|
def _has_handler(name: str) -> bool:
|
|
# Uvicorn imports the app during startup, so handlers must be idempotent.
|
|
return any(handler.name == name for handler in logging.getLogger().handlers)
|