feat: migrate Model Resolver logging system

This commit is contained in:
Dariusz L
2026-08-13 00:57:52 +02:00
parent cf17dda7d4
commit 2cec014d4c
23 changed files with 2978 additions and 1215 deletions
+15
View File
@@ -0,0 +1,15 @@
"""
@author: Azornes
@title: AzLogs
@version: 2.0.1
@description: Logging Initializer
"""
from .log_funcs import create_module_logger
from .logger import LogLevel, logger
__all__ = [
"LogLevel",
"create_module_logger",
"logger",
]
+18
View File
@@ -0,0 +1,18 @@
"""
@author: Azornes
@title: AzLogs
@version: 2.0.1
@description: Logging Configuration
"""
# Log level for development.
# Possible values: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL', 'NONE'
LOG_LEVEL = "ERROR"
# Logger module name. Set to None to auto-detect from folder name.
LOG_MODULE_NAME = "LayerForge"
# Enable ANSI colors in console logs.
USE_COLORS = True
# Project name aliases stripped during module name normalization.
PROJECT_ALIASES = ["Comfyui-LayerForge", "comfyui-layerforge", "layerforge"]
+201
View File
@@ -0,0 +1,201 @@
"""
@author: Azornes
@title: AzLogs
@version: 2.0.1
@description: Logging Initializer
"""
import logging
import os
import traceback
_logger = logging.getLogger(__name__)
_initialized = False
_project_root = None
_default_module_name = __name__
try:
from .config import LOG_LEVEL, LOG_MODULE_NAME, PROJECT_ALIASES, USE_COLORS
from .logger import LogLevel, debug, error, exception, fatal, info, logger, warn
def _find_project_root(start_path):
current = os.path.dirname(os.path.abspath(start_path))
while current:
if os.path.isdir(os.path.join(current, ".git")):
return current
parent = os.path.dirname(current)
if parent == current:
break
current = parent
return os.path.dirname(os.path.dirname(os.path.abspath(start_path)))
_project_root = _find_project_root(__file__)
_default_module_name = (
LOG_MODULE_NAME
if LOG_MODULE_NAME is not None
else os.path.basename(_project_root)
)
logger.set_global_level(LogLevel[LOG_LEVEL])
logger.configure(
{
"log_to_file": True,
"log_dir": os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs"),
"use_colors": (
logger.config["use_colors"]
if "AZLOGS_USE_COLORS" in os.environ
else USE_COLORS
),
}
)
_initialized = True
except ImportError as e:
_initialized = False
_logger.error(f"Failed to initialize logger: {e}")
# Provide fallback values when import fails
LOG_MODULE_NAME = None
PROJECT_ALIASES = []
def _normalize_module_name(module_name):
"""Normalize a dotted module name for display in logs.
Strips common prefixes (custom_nodes, project aliases) and ``__init__``
suffixes to produce a clean, readable module path.
ComfyUI sets ``__name__`` to hybrid values like
``E:\\...\\comfyui-model-resolver.core.sources.model_list`` where the
first dotted segment is a full OS path. We detect this and convert
the path portion to a dotted name before applying the normal logic.
"""
module_name = str(module_name or "").strip()
if not module_name or module_name == "__main__":
return _default_module_name
# --- path-to-dots conversion (required for ComfyUI's __name__) ---
if os.sep in module_name or "/" in module_name:
# Replace all path separators with dots so the rest of the logic
# can work uniformly on dotted segments.
normalized = module_name.replace("\\", ".").replace("/", ".")
# Remove drive letter prefix (e.g. "E:." → "")
if len(normalized) >= 2 and normalized[1] == ":":
normalized = normalized[2:].lstrip(".")
# Strip .py extension if present
if normalized.endswith(".py"):
normalized = normalized[:-3]
module_name = normalized
parts = [p for p in module_name.split(".") if p]
if not parts:
return _default_module_name
# Strip __init__ suffix
if parts[-1] == "__init__":
parts = parts[:-1]
# Strip LOG_MODULE_NAME prefix if present (re-added later)
if LOG_MODULE_NAME and parts and parts[0] == LOG_MODULE_NAME:
parts = parts[1:]
# Strip custom_nodes.<package_name> prefix
if "custom_nodes" in parts:
idx = parts.index("custom_nodes")
parts = parts[idx + 2:] # skip custom_nodes + package folder
# Strip project aliases from config
for alias in PROJECT_ALIASES:
if alias in parts:
parts = parts[parts.index(alias) + 1:]
break
# Re-insert LOG_MODULE_NAME prefix at the start
package_name = LOG_MODULE_NAME or os.path.basename(_project_root or "")
if package_name and (not parts or parts[0] != package_name):
parts.insert(0, package_name)
cleaned = ".".join(parts).strip(".")
return cleaned or _default_module_name
# Number of internal call frames between ModuleLogger methods and
# Python's logging.Logger.log(). Used to compute the correct
# ``stacklevel`` so that log records report the *caller's* file and
# line number instead of an internal wrapper.
#
# Call chain: ModuleLogger.method → module-level func → AzLogsLogger.log → logging.Logger.log
_INTERNAL_FRAMES = 3
class ModuleLogger:
"""Per-module logger with a fixed module name.
Usage:
log = create_module_logger(__name__)
log.debug("message")
log.info("message")
log.info("downloaded", extra={"model": "xyz"})
"""
def __init__(self, module_name):
self.module_name = module_name
def debug(self, *args, **kwargs):
if _initialized:
kwargs["stacklevel"] = kwargs.pop("stacklevel", 1) + _INTERNAL_FRAMES
debug(self.module_name, *args, **kwargs)
else:
print(f"[DEBUG] [{self.module_name}]", *args)
def info(self, *args, **kwargs):
if _initialized:
kwargs["stacklevel"] = kwargs.pop("stacklevel", 1) + _INTERNAL_FRAMES
info(self.module_name, *args, **kwargs)
else:
print(f"[INFO] [{self.module_name}]", *args)
def warning(self, *args, **kwargs):
if _initialized:
kwargs["stacklevel"] = kwargs.pop("stacklevel", 1) + _INTERNAL_FRAMES
warn(self.module_name, *args, **kwargs)
else:
print(f"[WARN] [{self.module_name}]", *args)
def warn(self, *args, **kwargs):
self.warning(*args, **kwargs)
def error(self, *args, **kwargs):
if _initialized:
kwargs["stacklevel"] = kwargs.pop("stacklevel", 1) + _INTERNAL_FRAMES
error(self.module_name, *args, **kwargs)
else:
print(f"[ERROR] [{self.module_name}]", *args)
def exception(self, *args, **kwargs):
if _initialized:
kwargs["stacklevel"] = kwargs.pop("stacklevel", 1) + _INTERNAL_FRAMES
exception(self.module_name, *args, **kwargs)
else:
print(f"[ERROR] [{self.module_name}]", *args)
traceback.print_exc()
def fatal(self, *args, **kwargs):
if _initialized:
kwargs["stacklevel"] = kwargs.pop("stacklevel", 1) + _INTERNAL_FRAMES
fatal(self.module_name, *args, **kwargs)
else:
print(f"[FATAL] [{self.module_name}]", *args)
def create_module_logger(module_name=None):
"""Create a ModuleLogger with a normalized module name.
Args:
module_name: Dotted module name, typically ``__name__``.
Required for proper module identification.
Returns:
A :class:`ModuleLogger` instance.
"""
resolved_name = _normalize_module_name(module_name or _default_module_name)
return ModuleLogger(resolved_name)
+658
View File
@@ -0,0 +1,658 @@
"""
@author: Azornes
@title: AzLogs
@version: 2.0.1
@description: Logging Setup - Central logging system
Features:
- Different log levels (DEBUG, INFO, WARN, ERROR)
- Ability to enable/disable logs globally or per module
- Colored logs in console
- Log file rotation
- Configuration via environment variables
"""
import json
import logging
import os
import re
import sys
import threading
import time
import traceback
from enum import IntEnum
from logging.handlers import RotatingFileHandler
# Log levels
class LogLevel(IntEnum):
DEBUG = 10
INFO = 20
WARN = 30
ERROR = 40
FATAL = 50
NONE = 100
# Level mapping
LEVEL_MAP = {
LogLevel.DEBUG: logging.DEBUG,
LogLevel.INFO: logging.INFO,
LogLevel.WARN: logging.WARNING,
LogLevel.ERROR: logging.ERROR,
LogLevel.FATAL: logging.CRITICAL,
LogLevel.NONE: logging.CRITICAL + 1,
}
# ANSI reset sequence (all coloring uses LEVEL_THEME RGB values)
ANSI_RESET = "\033[0m"
# RGB color theme per log level
LEVEL_THEME = {
"DEBUG": (155, 89, 182), # #9B59B6
"INFO": (46, 204, 113), # #2ECC71
"WARN": (243, 156, 18), # #F39C12
"ERROR": (192, 57, 43), # #C0392B
"FATAL": (231, 76, 60), # #E74C3C — brighter red than ERROR
}
# Default configuration
DEFAULT_CONFIG = {
"global_level": LogLevel.INFO,
"module_settings": {},
"use_colors": True,
"log_to_file": False,
"log_dir": "logs",
"max_file_size_mb": 10,
"backup_count": 5,
"timestamp_format": "%H:%M:%S",
"level_field_width": 5,
"source_field_width": 0,
}
_WINDOWS_VT_PROCESSING_ENABLED = False
def rotated_log_filename(default_name):
"""Keep the .log extension at the end of rotated log filenames."""
match = re.fullmatch(r"(.+)\.log\.(\d+)", str(default_name or ""))
if not match:
return default_name
return f"{match.group(1)}.{match.group(2)}.log"
def parse_rotated_log_filename(filename):
"""Return the log family and rotation number for the current filename style."""
name = os.path.basename(str(filename or ""))
match = re.fullmatch(r"(.+)\.(\d+)\.log", name)
if match:
return match.group(1), int(match.group(2))
if name.endswith(".log"):
return name[:-4], 0
return None
def _enable_windows_virtual_terminal_processing():
"""Enable ANSI escape sequence handling for classic Windows consoles."""
global _WINDOWS_VT_PROCESSING_ENABLED
if _WINDOWS_VT_PROCESSING_ENABLED or os.name != "nt":
return
try:
import ctypes
enable_virtual_terminal_processing = 0x0004
std_handles = (-11, -12)
invalid_handle_value = ctypes.c_void_p(-1).value
kernel32 = ctypes.windll.kernel32
kernel32.GetStdHandle.restype = ctypes.c_void_p
enabled_any = False
for std_handle in std_handles:
handle = kernel32.GetStdHandle(std_handle)
if handle in (None, 0, invalid_handle_value):
continue
mode = ctypes.c_uint32()
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
continue
new_mode = mode.value | enable_virtual_terminal_processing
if new_mode != mode.value:
kernel32.SetConsoleMode(handle, new_mode)
enabled_any = True
_WINDOWS_VT_PROCESSING_ENABLED = enabled_any
except Exception:
return
def _get_console_output_stream():
"""Return the console stream that preserves ANSI colors under wrapped launchers."""
for stream_name in ("__stdout__", "__stderr__", "stdout", "stderr"):
stream = getattr(sys, stream_name, None)
if stream is None:
continue
if getattr(stream, "closed", False):
continue
return stream
return None
class SafeRotatingFileHandler(RotatingFileHandler):
"""Rotating file handler that disables itself if a launcher breaks its stream."""
def __init__(self, *args, **kwargs):
self._disabled_by_stream_error = False
super().__init__(*args, **kwargs)
self.namer = rotated_log_filename
def emit(self, record):
if self._disabled_by_stream_error:
return
try:
if self.stream is not None and self.maxBytes > 0:
tell = getattr(self.stream, "tell", None)
if not callable(tell):
raise OSError(
"Log file stream does not support tell(); disabling file logs"
)
super().emit(record)
except Exception:
self.handleError(record)
def handleError(self, record):
self._disabled_by_stream_error = True
try:
self.close()
except Exception:
pass
class DirectConsoleHandler(logging.Handler):
"""Console handler that writes directly to the original console stream."""
terminator = "\n"
def __init__(self, stream=None):
super().__init__()
self.stream = stream
def emit(self, record):
try:
stream = self.stream or _get_console_output_stream()
if stream is None:
return
stream.write(self.format(record) + self.terminator)
stream.flush()
except Exception:
self.handleError(record)
class ColoredFormatter(logging.Formatter):
"""Formatter that adds colors to console logs"""
def __init__(
self,
fmt=None,
datefmt=None,
use_colors=True,
level_field_width=DEFAULT_CONFIG["level_field_width"],
source_field_width=DEFAULT_CONFIG["source_field_width"],
include_milliseconds=True,
include_brackets=True,
):
super().__init__(fmt, datefmt)
self.use_colors = use_colors
self.level_field_width = self._field_width(level_field_width)
self.source_field_width = self._field_width(source_field_width)
self.include_milliseconds = include_milliseconds
self.include_brackets = include_brackets
def format(self, record):
# Get the formatted message from the record
message = record.getMessage()
if record.exc_info:
message += "\n" + self.formatException(record.exc_info)
levelname = self._display_level_name(record.levelname)
level = self._format_level(levelname)
logger_name = self._display_logger_name(record.name)
logger_root, logger_detail = self._split_logger_name(logger_name)
source = (
f"({logger_detail}:{record.lineno})"
if logger_detail
else f"({logger_root}:{record.lineno})"
)
if self.source_field_width:
source = source.ljust(self.source_field_width)
timestamp = self._format_time(record)
separator = " "
root_label = logger_root
if self.include_brackets:
timestamp = f"[{timestamp}]"
root_label = f"[{root_label}]"
if self.use_colors:
level = self._color_level_badge(levelname, level)
timestamp = self._color_context_badge(timestamp)
root_label = self._color_context_badge(root_label)
separator = self._color_separator(levelname, separator)
message = self._color_message(levelname, message)
return f"{level}{timestamp}{separator}{root_label}{separator} {message} {source}"
return f"{level} {timestamp} {root_label} {message} {source}"
def _field_width(self, value):
try:
return max(0, int(value))
except (TypeError, ValueError):
return 0
def _display_logger_name(self, name):
return str(name or "").removeprefix("azlogs.")
def _split_logger_name(self, name):
parts = str(name or "").split(".", 1)
if len(parts) == 1:
return parts[0], ""
return parts[0], parts[1]
def _display_level_name(self, levelname):
name = str(levelname or "")
if name == "WARNING":
return "WARN"
if name == "CRITICAL":
return "FATAL"
return name
def _format_level(self, levelname):
if self.include_brackets:
width = self.level_field_width + 2
return f"[{levelname}]".ljust(width)
return levelname.ljust(self.level_field_width)
def _format_time(self, record):
timestamp = time.strftime(
self.datefmt or "%H:%M:%S", self.converter(record.created)
)
if self.include_milliseconds:
return f"{timestamp}.{int(record.msecs):03d}"
return timestamp
def _color_level_badge(self, levelname, text):
rgb = LEVEL_THEME.get(levelname)
if not rgb:
return text
r, g, b = rgb
return f"\033[1;97;48;2;{r};{g};{b}m {text} {ANSI_RESET}"
def _color_context_badge(self, text):
return f"\033[97;48;2;38;63;76m {text} {ANSI_RESET}"
def _color_separator(self, levelname, text):
rgb = LEVEL_THEME.get(levelname)
if not rgb:
return text
r, g, b = rgb
return f"\033[48;2;{r};{g};{b}m{text}{ANSI_RESET}"
def _color_message(self, levelname, text):
rgb = LEVEL_THEME.get(levelname)
if not rgb:
# Fallback: cyan for unknown levels
return f"\033[96m{text}{ANSI_RESET}"
r, g, b = rgb
return f"\033[38;2;{r};{g};{b}m{text}{ANSI_RESET}"
class AzLogsLogger:
"""Main logger class for AzLogs"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.config = DEFAULT_CONFIG.copy()
self.enabled = True
self.loggers = {}
self.file_handlers = {}
self._lock = threading.RLock()
# Load configuration from environment variables
self._load_config_from_env()
self._initialized = True
def _load_config_from_env(self):
"""Load configuration from environment variables"""
# Global level
if "AZLOGS_LOG_LEVEL" in os.environ:
level_name = os.environ["AZLOGS_LOG_LEVEL"].upper()
if hasattr(LogLevel, level_name):
self.config["global_level"] = getattr(LogLevel, level_name)
# Module settings
if "AZLOGS_MODULE_LEVELS" in os.environ:
try:
module_settings = json.loads(os.environ["AZLOGS_MODULE_LEVELS"])
for module, level_name in module_settings.items():
if hasattr(LogLevel, level_name.upper()):
self.config["module_settings"][module] = getattr(
LogLevel, level_name.upper()
)
except json.JSONDecodeError:
pass
# Other settings
if "AZLOGS_USE_COLORS" in os.environ:
self.config["use_colors"] = (
os.environ["AZLOGS_USE_COLORS"].lower() == "true"
)
if "AZLOGS_LOG_TO_FILE" in os.environ:
self.config["log_to_file"] = (
os.environ["AZLOGS_LOG_TO_FILE"].lower() == "true"
)
if "AZLOGS_LOG_DIR" in os.environ:
self.config["log_dir"] = os.environ["AZLOGS_LOG_DIR"]
if "AZLOGS_MAX_FILE_SIZE_MB" in os.environ:
try:
self.config["max_file_size_mb"] = int(
os.environ["AZLOGS_MAX_FILE_SIZE_MB"]
)
except ValueError:
pass
if "AZLOGS_BACKUP_COUNT" in os.environ:
try:
self.config["backup_count"] = int(os.environ["AZLOGS_BACKUP_COUNT"])
except ValueError:
pass
def configure(self, config):
"""Configure the logger"""
self.config.update(config)
self.reset_loggers()
if self.config.get("use_colors"):
_enable_windows_virtual_terminal_processing()
# If file logging is enabled, ensure the directory exists
if self.config.get("log_to_file") and self.config.get("log_dir"):
try:
os.makedirs(self.config["log_dir"], exist_ok=True)
except OSError as e:
# This is a fatal situation, so use print
print(
f"[FATAL] Could not create log directory: {self.config['log_dir']}. Error: {e}"
)
traceback.print_exc()
# Disable file logging to avoid further errors
self.config["log_to_file"] = False
return self
def set_enabled(self, enabled):
"""Enable/disable the logger globally"""
self.enabled = enabled
return self
def set_global_level(self, level):
"""Set global logging level"""
self.config["global_level"] = level
return self
def set_module_level(self, module, level):
"""Set logging level for a specific module"""
self.config["module_settings"][module] = level
return self
def is_level_enabled(self, module, level):
"""Check if a given logging level is active for the module"""
if not self.enabled:
return False
# Determine effective logging level, considering module and global settings
effective_level = self.config["module_settings"].get(
module, self.config["global_level"]
)
# If effective level is NONE, logging is completely disabled
if effective_level == LogLevel.NONE:
return False
# Otherwise check if log level is high enough
return level >= effective_level
def _sanitize_logger_name(self, value):
"""Sanitize logger/module names for safe filesystem usage."""
sanitized = re.sub(r'[<>:"/\\|?*\s]+', "_", str(value or "")).strip("._")
return sanitized or "default"
def _root_module_name(self, module):
"""Return the top-level logger name used for shared log files."""
return str(module or "default").split(".", 1)[0] or "default"
def _get_log_file(self, module):
safe_root = self._sanitize_logger_name(self._root_module_name(module))
return os.path.join(self.config["log_dir"], f"azlogs_{safe_root}.log")
def _get_file_handler(self, module):
"""Get or create a shared rotating file handler for the root module."""
log_file = self._get_log_file(module)
# Fast path without lock
if log_file in self.file_handlers:
return self.file_handlers[log_file]
with self._lock:
# Double-check after acquiring lock
if log_file in self.file_handlers:
return self.file_handlers[log_file]
os.makedirs(self.config["log_dir"], exist_ok=True)
file_handler = SafeRotatingFileHandler(
log_file,
maxBytes=self.config["max_file_size_mb"] * 1024 * 1024,
backupCount=self.config["backup_count"],
encoding="utf-8",
)
file_formatter = ColoredFormatter(
datefmt="%Y-%m-%d %H:%M:%S",
use_colors=False,
source_field_width=self.config["source_field_width"],
include_milliseconds=True,
)
file_handler.setFormatter(file_formatter)
self.file_handlers[log_file] = file_handler
return file_handler
def reset_loggers(self):
"""Remove configured handlers so new settings are applied cleanly."""
with self._lock:
handlers_to_close = set()
for module in self.loggers:
configured_logger = logging.getLogger(f"azlogs.{module}")
for handler in list(configured_logger.handlers):
configured_logger.removeHandler(handler)
if isinstance(handler, RotatingFileHandler):
handlers_to_close.add(handler)
for handler in self.file_handlers.values():
handlers_to_close.add(handler)
for handler in handlers_to_close:
handler.close()
self.loggers = {}
self.file_handlers = {}
return self
def _get_logger(self, module):
"""Get or create a logger for the module"""
# Fast path without lock
if module in self.loggers:
return self.loggers[module]
with self._lock:
# Double-check after acquiring lock
if module in self.loggers:
return self.loggers[module]
# Create new logger
new_logger = logging.getLogger(f"azlogs.{module}")
new_logger.setLevel(logging.DEBUG) # Set lowest level, filtering will be done later
new_logger.propagate = False
# Add console handler
if self.config["use_colors"]:
_enable_windows_virtual_terminal_processing()
console_stream = _get_console_output_stream()
console_handler = DirectConsoleHandler(console_stream)
console_formatter = ColoredFormatter(
fmt="[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s",
datefmt=self.config["timestamp_format"],
use_colors=self.config["use_colors"],
level_field_width=self.config["level_field_width"],
source_field_width=self.config["source_field_width"],
include_brackets=False,
)
console_handler.setFormatter(console_formatter)
new_logger.addHandler(console_handler)
# Add file handler if file logging is enabled
if self.config["log_to_file"]:
try:
file_handler = self._get_file_handler(module)
new_logger.addHandler(file_handler)
except OSError as e:
self.config["log_to_file"] = False
new_logger.warning(
"AzLogs: disabling file logging after handler setup failed for %s: %s",
self._get_log_file(module),
e,
)
self.loggers[module] = new_logger
return new_logger
def log(self, module, level, *args, **kwargs):
"""Write log"""
if not self.is_level_enabled(module, level):
return
logger = self._get_logger(module)
# Convert arguments to string
message = " ".join(str(arg) for arg in args)
# Extract known kwargs
exc_info = kwargs.get("exc_info")
stacklevel = kwargs.get("stacklevel", 1)
extra = kwargs.get("extra")
# Map LogLevel to logging level
log_level = LEVEL_MAP.get(level, logging.INFO)
# Write log
logger.log(log_level, message, exc_info=exc_info, stacklevel=stacklevel, extra=extra)
def debug(self, module, *args, **kwargs):
"""Log at DEBUG level"""
self.log(module, LogLevel.DEBUG, *args, **kwargs)
def info(self, module, *args, **kwargs):
"""Log at INFO level"""
self.log(module, LogLevel.INFO, *args, **kwargs)
def warn(self, module, *args, **kwargs):
"""Log at WARN level"""
self.log(module, LogLevel.WARN, *args, **kwargs)
def error(self, module, *args, **kwargs):
"""Log at ERROR level"""
self.log(module, LogLevel.ERROR, *args, **kwargs)
def exception(self, module, *args, **kwargs):
"""Log exception at ERROR level"""
kwargs["exc_info"] = True
self.log(module, LogLevel.ERROR, *args, **kwargs)
def fatal(self, module, *args, **kwargs):
"""Log at FATAL level"""
self.log(module, LogLevel.FATAL, *args, **kwargs)
# Singleton
logger = AzLogsLogger()
# Helper functions
def debug(module, *args, **kwargs):
"""Log at DEBUG level"""
logger.log(module, LogLevel.DEBUG, *args, **kwargs)
def info(module, *args, **kwargs):
"""Log at INFO level"""
logger.log(module, LogLevel.INFO, *args, **kwargs)
def warn(module, *args, **kwargs):
"""Log at WARN level"""
logger.log(module, LogLevel.WARN, *args, **kwargs)
def error(module, *args, **kwargs):
"""Log at ERROR level"""
logger.log(module, LogLevel.ERROR, *args, **kwargs)
def exception(module, *args, **kwargs):
"""Log exception at ERROR level"""
kwargs["exc_info"] = True
logger.log(module, LogLevel.ERROR, *args, **kwargs)
def fatal(module, *args, **kwargs):
"""Log at FATAL level"""
logger.log(module, LogLevel.FATAL, *args, **kwargs)
# Function to quickly enable/disable debugging
def set_debug(enabled=True):
"""Enable/disable debugging globally"""
if enabled:
logger.set_global_level(LogLevel.DEBUG)
else:
logger.set_global_level(LogLevel.INFO)
return logger
# Function to enable/disable file logging
def set_file_logging(enabled=True, log_dir=None):
"""Enable/disable logging to file"""
logger.config["log_to_file"] = enabled
if log_dir:
logger.config["log_dir"] = log_dir
os.makedirs(log_dir, exist_ok=True)
# Reset loggers to apply new settings
logger.reset_loggers()
return logger
+54 -318
View File
@@ -1,323 +1,59 @@
"""
Logger - Centralny system logowania dla ComfyUI-LayerForge (Python)
"""Backward-compatible exports for the LayerForge logging system.
Funkcje:
- Różne poziomy logowania (DEBUG, INFO, WARN, ERROR)
- Możliwość włączania/wyłączania logów globalnie lub per moduł
- Kolorowe logi w konsoli
- Rotacja plików logów
- Konfiguracja przez zmienne środowiskowe
The implementation lives in :mod:`python.log_system`, matching the Model
Resolver layout. Existing LayerForge imports can continue using
``python.logger`` while all callers share the same logger instance.
"""
import os
import sys
import json
import logging
import datetime
from enum import IntEnum
from logging.handlers import RotatingFileHandler
import traceback
from .log_system.logger import (
ANSI_RESET,
DEFAULT_CONFIG,
LEVEL_MAP,
LEVEL_THEME,
AzLogsLogger,
ColoredFormatter,
DirectConsoleHandler,
LogLevel,
SafeRotatingFileHandler,
logger,
parse_rotated_log_filename,
rotated_log_filename,
set_debug,
set_file_logging,
)
from .log_system.log_funcs import (
create_module_logger,
debug,
error,
exception,
fatal,
info,
warn,
)
# Poziomy logowania
class LogLevel(IntEnum):
DEBUG = 10
INFO = 20
WARN = 30
ERROR = 40
NONE = 100
LayerForgeLogger = AzLogsLogger
# Mapowanie poziomów logowania
LEVEL_MAP = {
LogLevel.DEBUG: logging.DEBUG,
LogLevel.INFO: logging.INFO,
LogLevel.WARN: logging.WARNING,
LogLevel.ERROR: logging.ERROR,
LogLevel.NONE: logging.CRITICAL + 1
}
# Kolory ANSI dla różnych poziomów logowania
COLORS = {
LogLevel.DEBUG: '\033[90m', # Szary
LogLevel.INFO: '\033[94m', # Niebieski
LogLevel.WARN: '\033[93m', # Żółty
LogLevel.ERROR: '\033[91m', # Czerwony
'RESET': '\033[0m' # Reset
}
# Konfiguracja domyślna
DEFAULT_CONFIG = {
'global_level': LogLevel.INFO,
'module_settings': {},
'use_colors': True,
'log_to_file': False,
'log_dir': 'logs',
'max_file_size_mb': 10,
'backup_count': 5,
'timestamp_format': '%H:%M:%S',
}
class ColoredFormatter(logging.Formatter):
"""Formatter dodający kolory do logów w konsoli"""
def __init__(self, fmt=None, datefmt=None, use_colors=True):
super().__init__(fmt, datefmt)
self.use_colors = use_colors
def format(self, record):
# Get the formatted message from the record
message = record.getMessage()
if record.exc_info:
message += "\n" + self.formatException(record.exc_info)
levelname = record.levelname
# Build the log prefix
prefix = '[{}] [{}] [{}]'.format(
self.formatTime(record, self.datefmt),
record.name,
record.levelname
)
# Apply color and bold styling to the prefix
if self.use_colors and hasattr(LogLevel, levelname):
level_enum = getattr(LogLevel, levelname)
if level_enum in COLORS:
# Apply bold (\033[1m) and color, then reset
prefix = f"\033[1m{COLORS[level_enum]}{prefix}{COLORS['RESET']}"
return f"{prefix} {message}"
class LayerForgeLogger:
"""Główna klasa loggera dla LayerForge"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(LayerForgeLogger, cls).__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.config = DEFAULT_CONFIG.copy()
self.enabled = True
self.loggers = {}
# Załaduj konfigurację ze zmiennych środowiskowych
self._load_config_from_env()
self._initialized = True
def _load_config_from_env(self):
"""Załaduj konfigurację ze zmiennych środowiskowych"""
# Poziom globalny
if 'LAYERFORGE_LOG_LEVEL' in os.environ:
level_name = os.environ['LAYERFORGE_LOG_LEVEL'].upper()
if hasattr(LogLevel, level_name):
self.config['global_level'] = getattr(LogLevel, level_name)
# Ustawienia modułów
if 'LAYERFORGE_MODULE_LEVELS' in os.environ:
try:
module_settings = json.loads(os.environ['LAYERFORGE_MODULE_LEVELS'])
for module, level_name in module_settings.items():
if hasattr(LogLevel, level_name.upper()):
self.config['module_settings'][module] = getattr(LogLevel, level_name.upper())
except json.JSONDecodeError:
pass
# Inne ustawienia
if 'LAYERFORGE_USE_COLORS' in os.environ:
self.config['use_colors'] = os.environ['LAYERFORGE_USE_COLORS'].lower() == 'true'
if 'LAYERFORGE_LOG_TO_FILE' in os.environ:
self.config['log_to_file'] = os.environ['LAYERFORGE_LOG_TO_FILE'].lower() == 'true'
if 'LAYERFORGE_LOG_DIR' in os.environ:
self.config['log_dir'] = os.environ['LAYERFORGE_LOG_DIR']
if 'LAYERFORGE_MAX_FILE_SIZE_MB' in os.environ:
try:
self.config['max_file_size_mb'] = int(os.environ['LAYERFORGE_MAX_FILE_SIZE_MB'])
except ValueError:
pass
if 'LAYERFORGE_BACKUP_COUNT' in os.environ:
try:
self.config['backup_count'] = int(os.environ['LAYERFORGE_BACKUP_COUNT'])
except ValueError:
pass
def configure(self, config):
"""Konfiguracja loggera"""
self.config.update(config)
# Jeśli włączono logowanie do pliku, upewnij się, że katalog istnieje
if self.config.get('log_to_file') and self.config.get('log_dir'):
try:
os.makedirs(self.config['log_dir'], exist_ok=True)
except OSError as e:
# To jest sytuacja krytyczna, więc użyjmy print
print(f"[CRITICAL] Could not create log directory: {self.config['log_dir']}. Error: {e}")
traceback.print_exc()
# Wyłącz logowanie do pliku, aby uniknąć dalszych błędów
self.config['log_to_file'] = False
return self
def set_enabled(self, enabled):
"""Włącz/wyłącz logger globalnie"""
self.enabled = enabled
return self
def set_global_level(self, level):
"""Ustaw globalny poziom logowania"""
self.config['global_level'] = level
return self
def set_module_level(self, module, level):
"""Ustaw poziom logowania dla konkretnego modułu"""
self.config['module_settings'][module] = level
return self
def is_level_enabled(self, module, level):
"""Sprawdź, czy dany poziom logowania jest aktywny dla modułu"""
if not self.enabled:
return False
# Ustal efektywny poziom logowania, biorąc pod uwagę ustawienia modułu i globalne
effective_level = self.config['module_settings'].get(module, self.config['global_level'])
# Jeśli efektywny poziom to NONE, logowanie jest całkowicie wyłączone
if effective_level == LogLevel.NONE:
return False
# W przeciwnym razie sprawdź, czy poziom loga jest wystarczająco wysoki
return level >= effective_level
def _get_logger(self, module):
"""Pobierz lub utwórz logger dla modułu"""
if module in self.loggers:
return self.loggers[module]
# Utwórz nowy logger
logger = logging.getLogger(f"layerforge.{module}")
logger.setLevel(logging.DEBUG) # Ustaw najniższy poziom, filtrowanie będzie później
logger.propagate = False
# Dodaj handler dla konsoli
console_handler = logging.StreamHandler(sys.stdout)
console_formatter = ColoredFormatter(
fmt='[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s',
datefmt=self.config['timestamp_format'],
use_colors=self.config['use_colors']
)
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
# Dodaj handler dla pliku, jeśli włączono logowanie do pliku
if self.config['log_to_file']:
log_file = os.path.join(self.config['log_dir'], f"layerforge_{module}.log")
file_handler = RotatingFileHandler(
log_file,
maxBytes=self.config['max_file_size_mb'] * 1024 * 1024,
backupCount=self.config['backup_count'],
encoding='utf-8'
)
file_formatter = logging.Formatter(
fmt='[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
self.loggers[module] = logger
return logger
def log(self, module, level, *args, **kwargs):
"""Zapisz log"""
if not self.is_level_enabled(module, level):
return
logger = self._get_logger(module)
# Konwertuj argumenty na string
message = " ".join(str(arg) for arg in args)
# Dodaj informacje o wyjątku, jeśli podano
exc_info = kwargs.get('exc_info', None)
# Mapuj poziom LogLevel na poziom logging
log_level = LEVEL_MAP.get(level, logging.INFO)
# Zapisz log
logger.log(log_level, message, exc_info=exc_info)
def debug(self, module, *args, **kwargs):
"""Log na poziomie DEBUG"""
self.log(module, LogLevel.DEBUG, *args, **kwargs)
def info(self, module, *args, **kwargs):
"""Log na poziomie INFO"""
self.log(module, LogLevel.INFO, *args, **kwargs)
def warn(self, module, *args, **kwargs):
"""Log na poziomie WARN"""
self.log(module, LogLevel.WARN, *args, **kwargs)
def error(self, module, *args, **kwargs):
"""Log na poziomie ERROR"""
self.log(module, LogLevel.ERROR, *args, **kwargs)
def exception(self, module, *args):
"""Log wyjątku na poziomie ERROR"""
self.log(module, LogLevel.ERROR, *args, exc_info=True)
# Singleton
logger = LayerForgeLogger()
# Funkcje pomocnicze
def debug(module, *args, **kwargs):
"""Log na poziomie DEBUG"""
logger.debug(module, *args, **kwargs)
def info(module, *args, **kwargs):
"""Log na poziomie INFO"""
logger.info(module, *args, **kwargs)
def warn(module, *args, **kwargs):
"""Log na poziomie WARN"""
logger.warn(module, *args, **kwargs)
def error(module, *args, **kwargs):
"""Log na poziomie ERROR"""
logger.error(module, *args, **kwargs)
def exception(module, *args):
"""Log wyjątku na poziomie ERROR"""
logger.exception(module, *args)
# Funkcja do szybkiego włączania/wyłączania debugowania
def set_debug(enabled=True):
"""Włącz/wyłącz debugowanie globalnie"""
if enabled:
logger.set_global_level(LogLevel.DEBUG)
else:
logger.set_global_level(LogLevel.INFO)
return logger
# Funkcja do włączania/wyłączania logowania do pliku
def set_file_logging(enabled=True, log_dir=None):
"""Włącz/wyłącz logowanie do pliku"""
logger.config['log_to_file'] = enabled
if log_dir:
logger.config['log_dir'] = log_dir
os.makedirs(log_dir, exist_ok=True)
# Zresetuj loggery, aby zastosować nowe ustawienia
logger.loggers = {}
return logger
__all__ = [
"ANSI_RESET",
"DEFAULT_CONFIG",
"LEVEL_MAP",
"LEVEL_THEME",
"AzLogsLogger",
"ColoredFormatter",
"DirectConsoleHandler",
"LayerForgeLogger",
"LogLevel",
"SafeRotatingFileHandler",
"create_module_logger",
"debug",
"error",
"exception",
"fatal",
"info",
"logger",
"parse_rotated_log_filename",
"rotated_log_filename",
"set_debug",
"set_file_logging",
"warn",
]