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

1"""CLI diagnostics routing: keep answer output clean, log records go to a file.""" 

2 

3from __future__ import annotations 

4 

5import logging 

6import sys 

7from logging.handlers import RotatingFileHandler 

8from pathlib import Path 

9 

10from lilbee.core.config import cfg 

11 

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" 

17 

18 

19class _DiagnosticsRouter: 

20 """Holds whether the user asked for a log level this invocation.""" 

21 

22 def __init__(self) -> None: 

23 self.explicit_verbosity = False 

24 

25 

26_router = _DiagnosticsRouter() 

27 

28 

29def set_explicit_verbosity(explicit: bool) -> None: 

30 """Record whether the user requested a log level for this invocation.""" 

31 _router.explicit_verbosity = explicit 

32 

33 

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) 

50 

51 

52def route_diagnostics_to_log_file() -> Path | None: 

53 """Send log records and Python warnings to ``cfg.data_root/logs/cli.log``. 

54 

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