ap_scanner/log.py (1929B)
1 """Logging configuration for AP Scanner."""
2
3 import logging
4 import sys
5
6 # Stores the stderr handler's original level so it can be restored after scan.
7 _stderr_original_level: int = logging.INFO
8
9
10 def setup_logging(verbose: bool = False, log_file: str | None = None) -> None:
11 """Configure logging for ap_scanner.
12
13 Logs go to stderr (not stdout, which is reserved for the ANSI display).
14 When a log_file is specified, DEBUG-level output is always written there.
15 """
16 global _stderr_original_level
17 level = logging.DEBUG if verbose else logging.INFO
18 _stderr_original_level = level
19 fmt = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
20 datefmt = "%Y-%m-%d %H:%M:%S"
21
22 root = logging.getLogger("ap_scanner")
23 root.setLevel(logging.DEBUG)
24 root.handlers.clear()
25
26 # stderr handler — WARNING+ during scan to avoid ANSI display corruption
27 stderr_handler = logging.StreamHandler(sys.stderr)
28 stderr_handler.setLevel(level)
29 stderr_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
30 root.addHandler(stderr_handler)
31
32 if log_file:
33 file_handler = logging.FileHandler(log_file, encoding="utf-8")
34 file_handler.setLevel(logging.DEBUG)
35 file_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
36 root.addHandler(file_handler)
37
38
39 def suppress_stderr_during_scan() -> None:
40 """Raise stderr handler level to avoid corrupting the ANSI display."""
41 root = logging.getLogger("ap_scanner")
42 for h in root.handlers:
43 if isinstance(h, logging.StreamHandler) and h.stream is sys.stderr:
44 h.setLevel(logging.CRITICAL)
45
46
47 def restore_stderr_after_scan() -> None:
48 """Restore stderr handler to its original level."""
49 root = logging.getLogger("ap_scanner")
50 for h in root.handlers:
51 if isinstance(h, logging.StreamHandler) and h.stream is sys.stderr:
52 h.setLevel(_stderr_original_level)