blob: 37deb561db5a0641c1ffecd527c38138fc792431 [file] [log] [blame]
# Copyright 2025 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import functools
import logging
import sys
from typing import Any, Optional
_logger: Optional[logging.Logger] = None
_EXCEPTION = logging.CRITICAL + 1
class ColoredFormatter(logging.Formatter):
"""A logging formatter that adds colors to the output."""
COLORS = {
"DEBUG": "\033[94m", # Blue
"INFO": "\033[92m", # Green
"WARNING": "\033[93m", # Yellow
"ERROR": "\033[91m", # Red
"CRITICAL": "\033[1;91m", # Bold Red
}
RESET = "\033[0m"
def __init__(self, fmt: str) -> None:
super().__init__(fmt)
def format(self, record: logging.LogRecord) -> str:
log_message = super().format(record)
# Don't colorize exception messages.
if record.exc_info:
return log_message
color = self.COLORS.get(record.levelname)
if color:
return f"{color}{log_message}{self.RESET}"
return log_message
def init_logger(level: int = logging.INFO, colors: bool = False) -> None:
"""
Initializes the global logger.
Args:
level: The minimum log level to display.
colors: Whether to use colored output.
"""
global _logger
logger = logging.getLogger("cog")
# Clear any existing handlers. This can happen if log() is called before
# init_logger(), which causes may cause duplicate output if we do not clear
# the old logger handler.
logger.handlers.clear()
logger.setLevel(level)
handler = logging.StreamHandler(sys.stdout)
formatter: logging.Formatter
if colors:
formatter = ColoredFormatter(
"%(levelname)s: [%(filename)s:%(lineno)d] %(message)s"
)
else:
formatter = logging.Formatter(
"%(levelname)s: [%(filename)s:%(lineno)d] %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
_logger = logger
def log(level: int, *args: Any, **kwargs: Any) -> None:
"""
Logs a message with the given level.
This function has an API similar to the print function.
Args:
level: The log level (e.g., logging.INFO, 'INFO', 'DEBUG').
*args: The message parts to log.
**kwargs: Supports 'sep' to specify a separator.
Raises:
Exception: If the logger has not been initialized.
ValueError: If the log level is invalid.
"""
if _logger is None:
init_logger(logging.WARNING, colors=False)
# Needed to make mypy happy
assert _logger
# The stacklevel keyword argument allows wrappers to adjust which stack frame
# is reported as the source of the log message. We default to 1, which refers
# to the caller of this function. We add 1 to this value later to account
# for this wrapper function.
stacklevel = kwargs.pop("stacklevel", 1)
sep = kwargs.get("sep", " ")
message = sep.join(map(str, args))
if level == _EXCEPTION:
_logger.exception(message, stacklevel=stacklevel + 1)
return
_logger.log(level, message, stacklevel=stacklevel + 1)
def _add_stacklevel(func: Any) -> Any:
"""Decorator to increase the stacklevel for log helper functions."""
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
# Increase stacklevel so that the log message is attributed to the caller
# of the helper function, rather than the helper itself. We add 2 to
# account for this wrapper and the decorated log helper.
kwargs["stacklevel"] = kwargs.get("stacklevel", 1) + 2
return func(*args, **kwargs)
return wrapper
@_add_stacklevel
def log_info(*args: Any, **kwargs: Any) -> None:
"""
Logs a message with level INFO.
This function has an API similar to the print function.
Args:
*args: The message parts to log.
**kwargs: Supports 'sep' to specify a separator.
"""
log(logging.INFO, *args, **kwargs)
@_add_stacklevel
def log_debug(*args: Any, **kwargs: Any) -> None:
"""
Logs a message with level DEBUG.
This function has an API similar to the print function.
Args:
*args: The message parts to log.
**kwargs: Supports 'sep' to specify a separator.
"""
log(logging.DEBUG, *args, **kwargs)
@_add_stacklevel
def log_warn(*args: Any, **kwargs: Any) -> None:
"""
Logs a message with level WARNING.
This function has an API similar to the print function.
Args:
*args: The message parts to log.
**kwargs: Supports 'sep' to specify a separator.
"""
log(logging.WARNING, *args, **kwargs)
@_add_stacklevel
def log_error(*args: Any, **kwargs: Any) -> None:
"""
Logs a message with level ERROR.
This function has an API similar to the print function.
Args:
*args: The message parts to log.
**kwargs: Supports 'sep' to specify a separator.
"""
log(logging.ERROR, *args, **kwargs)
@_add_stacklevel
def log_critical(*args: Any, **kwargs: Any) -> None:
"""
Logs a message with level CRITICAL.
This function has an API similar to the print function.
Args:
*args: The message parts to log.
**kwargs: Supports 'sep' to specify a separator.
"""
log(logging.CRITICAL, *args, **kwargs)
@_add_stacklevel
def log_exception(*args: Any, **kwargs: Any) -> None:
"""
Logs a message with level ERROR, including exception information.
This function has an API similar to the print function and should be called
from within an exception handler.
Args:
*args: The message parts to log.
**kwargs: Supports 'sep' to specify a separator.
"""
log(_EXCEPTION, *args, **kwargs)