pyodide/pyodide-build/pyodide_build/logger.py

149 lines
3.7 KiB
Python

import logging
import os
from collections.abc import Generator
from contextlib import contextmanager
from rich.console import Console
from rich.highlighter import NullHighlighter
from rich.logging import RichHandler
from rich.theme import Theme
IN_CI = "CI" in os.environ
COLOR_THEME = Theme(
{
"debug": "",
"info": "",
"warning": "bold yellow",
"error": "bold red",
"critical": "bold red",
}
)
LEVEL_STDOUT = logging.INFO - 5
LEVEL_STDERR = logging.INFO + 5
# Note: it seems like rich's auto terminal detection mechanism doesn't work well for CircleCI.
# so we manually disable it when running in CI.
class CIAwareConsole(Console):
@property
def is_terminal(self) -> bool:
"""Check if the console is writing to a terminal."""
return not IN_CI and super().is_terminal
class StdoutFilter(logging.Filter):
def filter(self, record):
return record.levelno in (LEVEL_STDOUT, logging.DEBUG, logging.INFO)
class StderrFilter(logging.Filter):
def filter(self, record):
return record.levelno in (
LEVEL_STDERR,
logging.WARNING,
logging.ERROR,
logging.CRITICAL,
)
class _Logger(logging.Logger):
def success(self, msg, *args, **kwargs):
self.info("[bold green]%s[/bold green]", msg, *args, **kwargs)
def stdout(self, msg, *args, **kwargs):
self.log(LEVEL_STDOUT, msg, *args, **kwargs)
def stderr(self, msg, *args, **kwargs):
self.log(LEVEL_STDERR, msg, *args, **kwargs)
class RichFormatter(logging.Formatter):
"""Colorize log messages based on log level."""
def format(self, records: logging.LogRecord) -> str:
levelname = records.levelname.lower()
records.msg = f"[{levelname}]{records.msg}[/{levelname}]"
formatted = super().format(records)
return formatted
console_stdout = CIAwareConsole(theme=COLOR_THEME)
console_stderr = CIAwareConsole(stderr=True, theme=COLOR_THEME)
def _get_logger(log_level: int) -> _Logger:
logger = _Logger(__name__)
logger.setLevel(log_level)
rich_handler_stdout = RichHandler(
console=console_stdout,
highlighter=NullHighlighter(),
show_time=False,
show_level=False,
show_path=False,
markup=True,
)
rich_handler_stdout.setFormatter(RichFormatter("%(message)s"))
rich_handler_stderr = RichHandler(
console=console_stderr,
highlighter=NullHighlighter(),
show_time=False,
show_level=False,
show_path=False,
markup=True,
)
rich_handler_stderr.setFormatter(RichFormatter("%(message)s"))
rich_handler_stdout.addFilter(StdoutFilter())
rich_handler_stderr.addFilter(StderrFilter())
logger.addHandler(rich_handler_stdout)
logger.addHandler(rich_handler_stderr)
return logger
logger = _get_logger(logging.INFO)
@contextmanager
def set_log_level(
_logger: logging.Logger, log_level: int | bool
) -> Generator[None, None, None]:
"""Set the log level.
Parameters
----------
logger
The logger to set the log level for.
log_level
If True, set the log level to DEBUG (verbose mode).
If given an integer, set the log level to that value.
"""
original_log_level = logger.level
if isinstance(log_level, bool):
log_level = logging.DEBUG if log_level else original_log_level
_logger.setLevel(log_level)
yield
_logger.setLevel(original_log_level)
if __name__ == "__main__":
# For testing the colors
logger.debug("debug")
logger.info("info")
logger.stdout("stdout")
logger.warning("warning")
logger.error("error")
logger.success("success")
logger.stderr("stderr")