Coverage for src/lilbee/cli/log_routing.py: 100%
42 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
1"""CLI diagnostics routing: keep answer output clean, log records go to a file."""
3from __future__ import annotations
5import logging
6import sys
7from logging.handlers import RotatingFileHandler
8from pathlib import Path
10from lilbee.core.config import cfg
12_LOG_DIR_NAME = "logs"
13_CLI_LOG_FILE_NAME = "cli.log"
14_MAX_BYTES = 1_048_576 # 1 MiB
15_BACKUP_COUNT = 5
16_LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s: %(message)s"
19class _DiagnosticsRouter:
20 """Holds whether the user asked for a log level this invocation."""
22 def __init__(self) -> None:
23 self.explicit_verbosity = False
26_router = _DiagnosticsRouter()
29def set_explicit_verbosity(explicit: bool) -> None:
30 """Record whether the user requested a log level for this invocation."""
31 _router.explicit_verbosity = explicit
34def attach_rotating_file_handler(
35 log_path: Path,
36 *,
37 max_bytes: int = _MAX_BYTES,
38 backup_count: int = _BACKUP_COUNT,
39 level: int = logging.NOTSET,
40) -> None:
41 """Add a RotatingFileHandler for *log_path* to the root logger. Idempotent."""
42 root = logging.getLogger()
43 for handler in root.handlers:
44 if isinstance(handler, RotatingFileHandler) and Path(handler.baseFilename) == log_path:
45 return
46 handler = RotatingFileHandler(log_path, maxBytes=max_bytes, backupCount=backup_count)
47 handler.setFormatter(logging.Formatter(_LOG_FORMAT))
48 handler.setLevel(level)
49 root.addHandler(handler)
52def route_diagnostics_to_log_file() -> Path | None:
53 """Send log records and Python warnings to ``cfg.data_root/logs/cli.log``.
55 Answer-facing commands (ask, search, chat) call this after their overrides
56 resolve the data root, so stdout carries only the answer. Warnings become
57 ``py.warnings`` log records via ``logging.captureWarnings``; nothing is
58 filtered and no logger level changes. Skipped entirely when --log-level or
59 LILBEE_LOG_LEVEL was given: diagnostics then stay on stderr as usual.
60 """
61 if _router.explicit_verbosity:
62 return None
63 log_dir = cfg.data_root / _LOG_DIR_NAME
64 try:
65 log_dir.mkdir(parents=True, exist_ok=True)
66 except OSError:
67 return None
68 log_path = log_dir / _CLI_LOG_FILE_NAME
69 attach_rotating_file_handler(log_path)
70 root = logging.getLogger()
71 for handler in list(root.handlers):
72 if (
73 isinstance(handler, logging.StreamHandler)
74 and not isinstance(handler, logging.FileHandler)
75 and handler.stream in (sys.stderr, sys.stdout)
76 ):
77 root.removeHandler(handler)
78 logging.captureWarnings(True)
79 return log_path