"""Logging module.""" import re import json from datetime import datetime from pathlib import Path import logging from logging.handlers import TimedRotatingFileHandler # https://pkg.go.dev/github.com/shafiqaimanx/pastax/colors STYLES = { "ENDC": "\033[0m", "BOLD": "\033[1m", "ITALIC": "\033[3m", "UNDERLINE": "\033[4m", "RED": "\033[31m", "GREEN": "\033[32m", "YELLOW": "\033[33m", "BLUE": "\033[34m", "MAGENTA": "\033[35m", "CYAN": "\033[36m", "DARKGRAY": "\033[90m", "LIGHTRED": "\033[91m", "PINK": "\033[95m", "FIREBRICK": "\033[38;5;124m", "ORANGERED": "\033[38;5;202m", "TOMATO": "\033[38;5;203m", "GRAPEFRUIT": "\033[38;5;208m", "DARKORANGE": "\033[38;5;214m", "OKRED": "\033[91m", "OKGREEN": "\033[92m", "OKYELLOW": "\033[93m", "OKBLUE": "\033[94m", "OKMAGENTA": "\033[95m", "OKCYAN": "\033[96m", None: "", } # With name version (for debugging) LOG_FORMAT = "%(asctime)s | %(name)-12s | %(levelname)-8s | %(message)s" # LOG_FORMAT = "%(asctime)s | %(levelname)-8s | %(message)s" LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" class ANSIColorRemovingFormatter(logging.Formatter): def format(self, record): formatted = super().format(record) return re.sub(r"\x1b\[[0-9;]*m", "", formatted) class TqdmLogger: def write(self, message: str): log_info(message.lstrip("\r\n")) def flush(self): pass def setup_advanced_logger( logger_name: str = None, log_level: str = "INFO", log_format: str = LOG_FORMAT, date_format: str = LOG_DATE_FORMAT, log_to_console: bool = True, log_to_file: bool = True, log_dir: str = "logs", file_rotation: str = "midnight", file_backup_count: int = 7, ) -> logging.Logger: """ Setup an advanced logger with flexible configuration options. Keeps colors in console output, removes them in file output. Args: logger_name (str): Name of the logger. If None, root logger is used. log_level (str): Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). log_format (str): Format string for log messages. date_format (str): Format string for timestamps in log messages. log_to_console (bool): Whether to log to console. log_to_file (bool): Whether to log to file. log_dir (str): Directory to store log files. log_file_prefix (str): Prefix for log file names. file_rotation (str): When to rotate the log file (e.g., 'midnight', 'h' for hourly). file_backup_count (int): Number of backup log files to keep. Returns: logging.Logger: Configured logger object. """ # Create logger logger = logging.getLogger(logger_name) logger.setLevel(getattr(logging, log_level.upper())) # 기존 핸들러 제거 if logger.hasHandlers(): logger.handlers.clear() # Create formatters color_formatter = logging.Formatter(log_format, datefmt=date_format) no_color_formatter = ANSIColorRemovingFormatter(log_format, datefmt=date_format) # Console handler (with colors) if log_to_console: console_handler = logging.StreamHandler() console_handler.setFormatter(color_formatter) logger.addHandler(console_handler) # File handler (without colors) if log_to_file: log_dir_path = Path(log_dir) log_dir_path.mkdir(parents=True, exist_ok=True) current_date = datetime.now().strftime("%Y-%m-%d") log_file_name = f"{current_date}.log" log_file_path = log_dir_path / log_file_name file_handler = TimedRotatingFileHandler( filename=log_file_path, when=file_rotation, backupCount=file_backup_count, ) file_handler.setFormatter(no_color_formatter) file_handler.flush = lambda: file_handler.stream.flush() logger.addHandler(file_handler) return logger def pretty_dict(s: str) -> str: """Pretty print dictionary. Args: s (str): The dictionary to pretty print. Returns: str: The pretty printed dictionary. """ json_str = json.dumps(s, indent=2, ensure_ascii=False) json_str = json_str.replace('\\"', "'") return json_str def slog( msg: str, style: str | None = None, level: str = "info", dump: bool = True, **kwargs ) -> str: """Stylish log message. Args: msg (str): The message to log. style (str): The style of the message. level (str): The log level. dump (bool): The dump flag. Returns: str: The stylish message. """ try: if dump: msg = pretty_dict(msg) msg = msg.strip('"') # remove redundant quotes except: pass stylish_msg = f"{STYLES['BOLD']}{STYLES[style]}{msg}{STYLES['ENDC']}" match level: case "info": logger.info(stylish_msg, **kwargs) case "error": logger.error(stylish_msg, **kwargs) case "warning": logger.warning(stylish_msg, **kwargs) case "debug": logger.debug(stylish_msg, **kwargs) case _: print(stylish_msg, **kwargs) return stylish_msg def log_info(msg: str, dump: bool = True, **kwargs) -> str: """Stylish info log. Args: msg (str): The message to log. dump (bool): The dump flag. Defaults to True. """ return slog(msg, style="GREEN", dump=dump, **kwargs) def log_success(msg: str, dump: bool = True, prefix: bool = True, **kwargs) -> str: """Stylish success log. Args: msg (str): The message to log. dump (bool): The dump flag. Defaults to True. prefix (bool): The prefix flag. Defaults to True. """ if prefix: msg = f"[SUCCESS] {msg}" return slog(msg, style="OKBLUE", dump=dump, **kwargs) def log_error( msg: str, dump: bool = False, prefix: bool = True, exc_info: Exception | None = None, **kwargs, ) -> str: """Stylish error log. Args: msg (str): The message to log. dump (bool): The dump flag. Defaults to True. """ if prefix: msg = f"[FAILED] {msg}" return slog( msg, style="TOMATO", level="error", dump=dump, exc_info=exc_info, **kwargs ) def log_warning(msg: str, dump: bool = False, prefix: bool = True, **kwargs) -> str: """Stylish warning log. Args: msg (str): The message to log. dump (bool): The dump flag. Defaults to True. """ if prefix: msg = f"[WARNING] {msg}" return slog(msg, style="GRAPEFRUIT", level="warning", dump=dump, **kwargs) def log_api(msg: str, error: bool = False, **kwargs) -> None: """Stylish api log. Args: msg (str): The message to log. error (bool): The error status of the API. Defaults to False. """ if error: log_error("Request API:") log_error(msg, dump=True) else: log_success("Request API:") log_success(msg) # Setup default logger logger = setup_advanced_logger() # Disable logging for specific modules for name in ("elastic_transport.transport", "urllib3.connectionpool", "httpx"): _logger = logging.getLogger(name) _logger.setLevel(logging.ERROR) # Get tqdm file tqdm_file = TqdmLogger() if __name__ == "__main__": log_info("This is an info message.") log_success("This is a success message.") log_error("This is an error message.") log_warning("This is a warning message.") log_api("This is an API message.") for style in STYLES: slog(f"This is a {style} message.", style=style)