govon-runtime / src /cli /log_config.py
github-actions
sync: 49c609b
15dcd94
"""CLI logging configuration for GovOn.
Follows the industry standard used by Claude Code, Codex CLI, and Gemini CLI:
- All internal/debug logs are written to files only (~/.govon/logs/)
- Zero console noise in normal operation
- Opt-in debug output via setup_logging(debug=True)
- 7-day log retention with automatic cleanup
"""
from __future__ import annotations
import sys
from pathlib import Path
from loguru import logger
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
_LOG_DIR = Path.home() / ".govon" / "logs"
_LOG_RETENTION_DAYS = 7
_LOG_FORMAT_FILE = (
"{time:YYYY-MM-DDTHH:mm:ss.SSSZ} | {level} | {name}:{function}:{line} | {message}"
)
_LOG_FORMAT_STDERR = "{time:HH:mm:ss} | {level} | {message}"
# ---------------------------------------------------------------------------
# Module-level initialization: silence loguru's default stderr handler
# immediately on import so no debug output leaks before setup_logging() runs.
# ---------------------------------------------------------------------------
logger.remove() # remove the default stderr handler (id=0)
def setup_logging(debug: bool = False) -> None:
"""Configure logging for the CLI process.
Parameters
----------
debug:
When True, also emit WARNING+ log records to stderr so the operator
can see internal diagnostics. When False (default), all log output
goes to the rotating file only — the terminal UI stays clean.
"""
_LOG_DIR.mkdir(parents=True, exist_ok=True)
# File handler: all levels, structured format, 7-day rotation
logger.add(
str(_LOG_DIR / "cli-{time:YYYY-MM-DD}.log"),
level="DEBUG",
format=_LOG_FORMAT_FILE,
rotation="00:00", # rotate at midnight
retention=f"{_LOG_RETENTION_DAYS} days",
encoding="utf-8",
enqueue=True, # non-blocking writes
catch=True, # suppress handler exceptions
)
if debug:
# Optional stderr handler for operator/developer use
logger.add(
sys.stderr,
level="WARNING",
format=_LOG_FORMAT_STDERR,
colorize=True,
catch=True,
)
def cleanup_old_logs() -> int:
"""Delete log files older than 7 days from the log directory.
Returns
-------
int
Number of files deleted.
"""
import time
if not _LOG_DIR.exists():
return 0
cutoff = time.time() - (_LOG_RETENTION_DAYS * 86_400)
deleted = 0
for log_file in _LOG_DIR.glob("cli-*.log"):
try:
if log_file.stat().st_mtime < cutoff:
log_file.unlink()
deleted += 1
except OSError:
pass # ignore permission errors or race conditions
return deleted