File size: 2,882 Bytes
15dcd94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
"""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