v0.156.1
 1import logging
 2import sys
 3from typing import TextIO
 4
 5from .filters import DebugInfoFilter, WarningErrorCriticalFilter
 6from .formatters import JSONFormatter, KeyValueFormatter
 7
 8
 9def attach_log_handlers(
10    *,
11    logger: logging.Logger,
12    info_stream: TextIO,
13    warning_stream: TextIO,
14    formatter: logging.Formatter,
15) -> None:
16    """Attach two handlers to a logger that split by log level.
17
18    INFO and below go to info_stream, WARNING and above go to warning_stream.
19    """
20    # DEBUG and INFO handler
21    info_handler = logging.StreamHandler(info_stream)
22    info_handler.addFilter(DebugInfoFilter())
23    info_handler.setFormatter(formatter)
24    logger.addHandler(info_handler)
25
26    # WARNING, ERROR, and CRITICAL handler
27    warning_handler = logging.StreamHandler(warning_stream)
28    warning_handler.addFilter(WarningErrorCriticalFilter())
29    warning_handler.setFormatter(formatter)
30    logger.addHandler(warning_handler)
31
32
33def create_log_formatter(log_format: str) -> logging.Formatter:
34    """Create a formatter based on the log format setting."""
35    match log_format:
36        case "json":
37            return JSONFormatter("%(json)s")
38        case "keyvalue":
39            return KeyValueFormatter("[%(levelname)s] %(message)s %(keyvalue)s")
40        case _:
41            raise ValueError(
42                f"Invalid LOG_FORMAT: {log_format!r}. Must be 'keyvalue' or 'json'."
43            )
44
45
46def configure_logging(
47    *,
48    plain_log_level: int | str,
49    app_log_level: int | str,
50    app_log_format: str,
51    log_stream: str = "split",
52) -> None:
53    # Determine which streams to use based on log_stream setting
54    if log_stream == "split":
55        info_stream = sys.stdout
56        warning_stream = sys.stderr
57    elif log_stream == "stdout":
58        info_stream = sys.stdout
59        warning_stream = sys.stdout
60    else:  # stderr (or any other value defaults to stderr for backwards compat)
61        info_stream = sys.stderr
62        warning_stream = sys.stderr
63
64    # Determine formatter based on app_log_format
65    formatter = create_log_formatter(app_log_format)
66
67    # Create and configure the plain logger with structured formatting.
68    from .logger import app_logger
69
70    plain_logger = logging.getLogger("plain")
71    plain_logger.setLevel(plain_log_level)
72    attach_log_handlers(
73        logger=plain_logger,
74        info_stream=info_stream,
75        warning_stream=warning_stream,
76        formatter=formatter,
77    )
78    plain_logger.propagate = False
79
80    # Configure the existing app_logger
81    app_logger.setLevel(app_log_level)
82    app_logger.propagate = False
83
84    attach_log_handlers(
85        logger=app_logger,
86        info_stream=info_stream,
87        warning_stream=warning_stream,
88        formatter=formatter,
89    )
90
91    # Register the app_logger in the logging system so getLogger("app") returns it
92    logging.root.manager.loggerDict["app"] = app_logger