92 lines
3.4 KiB
Python
92 lines
3.4 KiB
Python
"""
|
|
================================================================================
|
|
Quantum Terminal Consumer v2 — Logging setup
|
|
================================================================================
|
|
Centralized logging configuration. Adds a daily-rotating file handler so logs
|
|
survive across runs and can be attached to support tickets, while keeping the
|
|
console output identical to v1.
|
|
|
|
Log location: %APPDATA%\\<APP_DIR>\\logs\\backend-YYYY-MM-DD.log
|
|
Rotation: daily at midnight, 14 days retained.
|
|
Format: same as console — `YYYY-MM-DD HH:MM:SS [logger.name] LEVEL: msg`
|
|
|
|
Call setup_logging() ONCE at process startup — launcher.py does this.
|
|
================================================================================
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import logging.handlers
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Match the APP_DIR convention used elsewhere (license_client / data_sync_client)
|
|
APP_DIR = os.environ.get("MK_APP_DIR_NAME", "QuantumTerminal")
|
|
|
|
_LOG_FORMAT = "%(asctime)s [%(name)s] %(levelname)s: %(message)s"
|
|
_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
_RETENTION_DAYS = 14
|
|
_FILE_MAX_BYTES = 50 * 1024 * 1024 # 50 MB hard cap per file as a backstop
|
|
|
|
|
|
def _appdata_logs_dir() -> Path:
|
|
"""Resolve %APPDATA%\\<APP_DIR>\\logs\\ (or ~/.<app_dir>/logs/ off-Windows)."""
|
|
appdata = os.environ.get("APPDATA", "")
|
|
if appdata:
|
|
return Path(appdata) / APP_DIR / "logs"
|
|
return Path.home() / f".{APP_DIR.lower()}" / "logs"
|
|
|
|
|
|
def setup_logging(level: int = logging.INFO) -> Path:
|
|
"""Configure root logging once. Returns the log file path so callers can
|
|
print it on startup (`launcher.py` does)."""
|
|
log_dir = _appdata_logs_dir()
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
log_file = log_dir / "backend.log"
|
|
|
|
root = logging.getLogger()
|
|
root.setLevel(level)
|
|
|
|
# If already configured (e.g., re-import in tests), don't double-attach.
|
|
if any(getattr(h, "_mk_managed", False) for h in root.handlers):
|
|
return log_file
|
|
|
|
fmt = logging.Formatter(_LOG_FORMAT, datefmt=_DATE_FORMAT)
|
|
|
|
# Console handler — stderr keeps the existing capture path used by Electron.
|
|
ch = logging.StreamHandler(stream=sys.stderr)
|
|
ch.setLevel(level)
|
|
ch.setFormatter(fmt)
|
|
ch._mk_managed = True
|
|
root.addHandler(ch)
|
|
|
|
# Daily rotating file handler. Use TimedRotatingFileHandler (rolls at
|
|
# midnight) with 14 backups. Hard size cap as a backstop in case of
|
|
# log floods.
|
|
try:
|
|
fh = logging.handlers.TimedRotatingFileHandler(
|
|
filename=str(log_file),
|
|
when="midnight",
|
|
interval=1,
|
|
backupCount=_RETENTION_DAYS,
|
|
encoding="utf-8",
|
|
delay=True, # don't open until first emit (avoids a stale empty file)
|
|
)
|
|
fh.setLevel(level)
|
|
fh.setFormatter(fmt)
|
|
fh._mk_managed = True
|
|
root.addHandler(fh)
|
|
except Exception as e:
|
|
# File logging is a nice-to-have — don't kill startup if disk is read-only.
|
|
print(f"[logging] file handler unavailable: {e}", file=sys.stderr)
|
|
|
|
# uvicorn / fastapi loggers tend to set up their own handlers; let them
|
|
# bubble up to root by stripping their handlers and unsetting `propagate=False`.
|
|
for noisy in ("uvicorn", "uvicorn.error", "uvicorn.access", "fastapi"):
|
|
lg = logging.getLogger(noisy)
|
|
lg.handlers.clear()
|
|
lg.propagate = True
|
|
|
|
return log_file
|