Source code for utils.logger

"""
This logger file provides easy configurability of the root and named loggers, along with a color print function for console output.

NOTE: If you are on Windows versions prior to Windows 10, the Command Prompt and PowerShell do not support ANSI color codes. You would need to use third-party libraries like colorama to enable ANSI color support.
Please add the following colorama setting at the beginning of your code:

Installation:

.. code-block:: bash

    pip install colorama

Initialization:

.. code-block:: python

    import colorama
    colorama.init(autoreset=True)
"""

import logging
import sys
from typing import List, Tuple, Optional, Literal
import inspect
import os
from datetime import datetime
from adalflow.utils.global_config import get_adalflow_default_root_path

LOG_LEVELS = {
    "DEBUG": logging.DEBUG,
    "INFO": logging.INFO,
    "WARNING": logging.WARNING,
    "ERROR": logging.ERROR,
    "CRITICAL": logging.CRITICAL,
}

# color map, from color string to colorama.Fore colors, easier to use, reduce friction
COLOR_MAP = {
    "black": "\x1b[30m",
    "blue": "\x1b[34m",
    "cyan": "\x1b[36m",
    "green": "\x1b[32m",
    "magenta": "\x1b[35m",
    "red": "\x1b[31m",
    "white": "\x1b[37m",
    "yellow": "\x1b[33m",
}

log = logging.getLogger(__name__)


def _get_log_config(
    level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
    filepath: str = "./logs/app.log",
    enable_console: bool = True,
    enable_file: bool = True,
) -> Tuple[int, List[logging.Handler]]:
    r"""Helper function to get the default log configuration.

    We config logging with the following default settings:
    1. Enable both console and file output.
    2. Set up the default format: "%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d:%(funcName)s] - %(message)s".
    The format is: time, log level, filename(where the log is from), line number, function name, message.
    3. Set up the default date format: "%Y-%m-%d %H:%M:%S".

    Args:
        level (str): Log level. Defaults to "INFO". Options: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL".
        filepath (str): Name of the output log file. Defaults to "./logs/app.log".
        enable_console (bool): Control the console output. Defaults to True.
        enable_file (bool): Control the file output. Defaults to True.

    Returns:
        Tuple[int, List[logging.Handler]]: The logging level and the list of handlers.
    """

    def get_level(level: str) -> int:
        """Return the logging level constant based on a string."""
        return LOG_LEVELS.get(level.upper(), logging.INFO)

    # 2. Config the default format and style
    format = "%(asctime)s - %(module)s - %(levelname)s - [%(filename)s:%(lineno)d:%(funcName)s] - %(message)s"
    formatter = logging.Formatter(format, datefmt="%Y-%m-%d %H:%M:%S")

    # Set up the handler
    handlers: List[logging.Handler] = []
    if enable_console:
        handlers.append(logging.StreamHandler(sys.stdout))
    if enable_file:
        file_path: str = os.path.dirname(filepath)
        os.makedirs(file_path, exist_ok=True)
        handler = logging.FileHandler(filename=filepath, mode="a")
        handlers.append(handler)

    for h in handlers:
        h.setFormatter(formatter)

    return get_level(level), handlers


[docs] def get_logger( name: Optional[str] = None, level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", save_dir: Optional[str] = None, filename: Optional[str] = None, enable_console: bool = True, enable_file: bool = True, ) -> logging.Logger: """Get or configure a logger, including both the root logger and named logger. Args: name (Optional[str]): Name of the logger. Defaults to None which means root logger. level (str): Log level. Defaults to "INFO". Options: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL". save_dir (Optional[str]): Directory to save log files. Defaults to "./logs". filename (Optional[str]): Name of the output log file. Defaults to "lib.log" for root logger and "{name}.log" for named logger. enable_console (bool): Control the console output. Defaults to True. enable_file (bool): Control the file output. Defaults to True. Returns: logging.Logger: The logger with the specified configuration. Example: 1. Get the root logger and have it log to both console and file (~/.adalflow/logs/lib.log): .. code-block:: python from adalflow.utils.logger import get_logger root_logger = get_logger(enable_console=True, enable_file=True) 2. Get a named logger and have it log to both console and file (./logs/app.log): .. code-block:: python logger = get_logger(name="app", enable_console=True, enable_file=True) 3. Use two loggers, one for the library and one for the application: .. code-block:: python root_logger = enable_library_logging(level="DEBUG", enable_console=True, enable_file=True) logger = get_logger(name="app", level="DEBUG", enable_console=True, enable_file=True) logger.info("This is an application log, and it will be saved in app.log separately from the library log at lib.log.") """ # save_dir = save_dir or "./logs" save_dir = save_dir or get_adalflow_default_root_path() + "/logs" os.makedirs(save_dir, exist_ok=True) if name is None: filename = filename or "lib.log" else: filename = filename or f"{name}.log" filepath = os.path.join(save_dir, filename) config = _get_log_config( level=level, filepath=filepath, enable_console=enable_console, enable_file=enable_file, ) logger = logging.getLogger(name) logger.setLevel(config[0]) logger.handlers = [] # Clear existing handlers for handler in config[1]: logger.addHandler(handler) if name: logger.propagate = False # Do not propagate to the root logger return logger
[docs] def get_current_script_and_line() -> Tuple[str, str, int]: """Get the function name, script name, and line of where the print is called. Returns: function_name (str): the name of the function that is calling. script_name (str): the name of the script that is calling. line_number (str): the line number of the code that is calling. """ caller_frame = inspect.stack()[2] file_path, line_number, function_name = ( caller_frame.filename, caller_frame.lineno, caller_frame.function, ) script_name = os.path.basename(file_path) return function_name, script_name, line_number
[docs] def printc( text: str, color: Literal[ "black", "blue", "cyan", "green", "magenta", "red", "white", "yellow" ] = "cyan", ): """Color enhanced print function with logger-like format. LightRAG's customized print with colored text, position of code block the print is set, and current timestamp. Args: text (str): Text to be printed. color (str): Color of the text. Defaults to "cyan". Options: 'black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow'. Defaults to "cyan". Example: .. code-block:: python from utils.logger import colored_print printc("hello", color="green") printc("hello") """ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") function_name, script_name, line_number = get_current_script_and_line() color_code = COLOR_MAP.get(color, COLOR_MAP["cyan"]) print( f"{color_code}{timestamp} - [{script_name}:{line_number}:{function_name}] - {text}\033[0m" )
# \033[0m means reset, not impacting the next print texts