Source code for geos.utils.Logger
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
# SPDX-FileContributor: Martin Lemay
import logging
from typing import Any, Union
from typing_extensions import Self
__doc__ = """
Logger module manages logging tools.
Code was modified from <https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output>
"""
# Add the convenience method for the logger
[docs]
def results( self: logging.Logger, message: str, *args: Any, **kws: Any ) -> None:
"""Logs a message with the custom 'RESULTS' severity level.
This level is designed for summary information that should always be
visible, regardless of the logger's verbosity setting.
Args:
self (logging.Logger): The logger instance.
message (str): The primary log message, with optional format specifiers (e.g., "Found %d issues.").
*args: The arguments to be substituted into the `message` string.
**kws: Keyword arguments for special functionality.
"""
if self.isEnabledFor( RESULTS_LEVEL_NUM ):
self._log( RESULTS_LEVEL_NUM, message, args, **kws )
# Define logging levels at the module level so they are available for the Formatter class
DEBUG: int = logging.DEBUG
INFO: int = logging.INFO
WARNING: int = logging.WARNING
ERROR: int = logging.ERROR
CRITICAL: int = logging.CRITICAL
# Define and register the new level for check results
RESULTS_LEVEL_NUM: int = 60
RESULTS_LEVEL_NAME: str = "RESULTS"
logging.addLevelName( RESULTS_LEVEL_NUM, RESULTS_LEVEL_NAME )
logging.Logger.results = results # type: ignore[attr-defined]
# types redefinition to import logging.* from this module
Logger = logging.Logger # logger type
[docs]
class CustomLoggerFormatter( logging.Formatter ):
"""Custom formatter for the logger.
.. WARNING:: Colors do not work in the ouput message window of Paraview.
To use it:
.. code-block:: python
logger = logging.getLogger( "Logger name", use_color=False )
# Ensure handler is added only once, e.g., by checking logger.handlers
if not logger.handlers:
ch = logging.StreamHandler()
ch.setFormatter(CustomLoggerFormatter())
logger.addHandler(ch)
"""
# define color codes
green: str = "\x1b[32;20m"
grey: str = "\x1b[38;20m"
yellow: str = "\x1b[33;20m"
red: str = "\x1b[31;20m"
bold_red: str = "\x1b[31;1m"
reset: str = "\x1b[0m"
# define prefix of log messages
format1: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
format2: str = ( "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" )
format_results: str = "%(name)s - %(levelname)s - %(message)s"
#: format for each logger output type with colors
FORMATS_COLOR: dict[ int, str ] = {
DEBUG: grey + format2 + reset,
INFO: grey + format1 + reset,
WARNING: yellow + format1 + reset,
ERROR: red + format1 + reset,
CRITICAL: bold_red + format2 + reset,
RESULTS_LEVEL_NUM: green + format_results + reset,
}
#: format for each logger output type without colors (e.g., for Paraview)
FORMATS_PLAIN: dict[ int, str ] = {
DEBUG: format2,
INFO: format1,
WARNING: format1,
ERROR: format1,
CRITICAL: format2,
RESULTS_LEVEL_NUM: format_results,
}
# Pre-compiled formatters for efficiency
_compiled_formatters: dict[ int, logging.Formatter ] = {
level: logging.Formatter( fmt )
for level, fmt in FORMATS_PLAIN.items()
}
_compiled_color_formatters: dict[ int, logging.Formatter ] = {
level: logging.Formatter( fmt )
for level, fmt in FORMATS_COLOR.items()
}
def __init__( self: Self, use_color: bool = False ) -> None:
"""Initialize the log formatter.
Args:
use_color (bool): If True, use color-coded log formatters.
Defaults to False.
"""
super().__init__()
if use_color:
self.active_formatters = self._compiled_color_formatters
else:
self.active_formatters = self._compiled_formatters
[docs]
def format( self: Self, record: logging.LogRecord ) -> str:
"""Return the format according to input record.
Args:
record (logging.LogRecord): record
Returns:
str: format as a string
"""
# Defaulting to plain formatters as per original logic
log_fmt_obj: Union[ logging.Formatter, None ] = self.active_formatters.get( record.levelno )
if log_fmt_obj:
return log_fmt_obj.format( record )
else:
# Fallback for unknown levels or if a level is missing in the map
return logging.Formatter().format( record )
[docs]
def getLogger( title: str, use_color: bool = False ) -> Logger:
"""Return the Logger with pre-defined configuration.
This function is now idempotent regarding handler addition.
Calling it multiple times with the same title will return the same
logger instance without adding more handlers if one is already present.
Example:
.. code-block:: python
# module import
import Logger
# logger instanciation
logger :Logger.Logger = Logger.getLogger("My application")
# logger use
logger.debug("debug message")
logger.info("info message")
logger.warning("warning message")
logger.error("error message")
logger.critical("critical message")
logger.results("results message")
Args:
title (str): Name of the logger.
use_color (bool): If True, configure the logger to output with color.
Defaults to False.
Returns:
Logger: logger
"""
logger = logging.getLogger( title )
# Only configure the logger (add handlers, set level) if it hasn't been configured before.
if not logger.hasHandlers(): # More Pythonic way to check if logger.handlers is empty
logger.setLevel( INFO ) # Set the desired default level for this logger
# Create and add the stream handler
ch = logging.StreamHandler()
ch.setFormatter( CustomLoggerFormatter( use_color ) ) # Use your custom formatter
logger.addHandler( ch )
# Optional: Prevent messages from propagating to the root logger's handlers
logger.propagate = False
# If you need to ensure a certain level is set every time getLogger is called,
# even if handlers were already present, you can set the level outside the 'if' block.
# However, typically, setLevel is part of the initial handler configuration.
# logger.setLevel(INFO) # Uncomment if you need to enforce level on every call
return logger