Files

189 lines
7.0 KiB
Python

"""
Logging utility module using Python standard library logging with file location info.
"""
import logging
import logging.handlers
import sys
import os
import inspect
from datetime import datetime
import io
class LocationFormatter(logging.Formatter):
"""Custom formatter that adds file location information."""
def format(self, record):
# Use location info if provided via extra, otherwise extract from record
if not hasattr(record, 'location'):
# Fallback: extract from record itself
filename = os.path.basename(record.pathname) if record.pathname else "unknown"
function_name = record.funcName if record.funcName else "unknown"
line_number = record.lineno if record.lineno else 0
record.location = f"{filename}:{function_name}:{line_number}"
return super().format(record)
def setup_logger():
"""Setup the main logger with console and file handlers."""
# Create logger
logger = logging.getLogger('comfyui_copilot')
logger.setLevel(logging.DEBUG)
# Prevent duplicate logs
if logger.handlers:
return logger
# Console handler with safer encoding handling on Windows consoles
# Prefer reconfiguring the existing stderr to replace unencodable chars
if hasattr(sys.stderr, "reconfigure"):
try:
sys.stderr.reconfigure(errors="replace")
except Exception:
pass
console_handler = logging.StreamHandler(sys.stderr)
else:
# Fallback: wrap the underlying buffer with a TextIOWrapper that replaces errors
try:
encoding = getattr(sys.stderr, "encoding", None) or "utf-8"
console_stream = io.TextIOWrapper(sys.stderr.buffer, encoding=encoding, errors="replace")
console_handler = logging.StreamHandler(console_stream)
except Exception:
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setLevel(logging.DEBUG)
# Console formatter with colors (simple format for better compatibility)
console_format = '%(asctime)s | %(levelname)-8s | %(location)s | %(message)s'
console_formatter = LocationFormatter(console_format, datefmt='%Y-%m-%d %H:%M:%S')
console_handler.setFormatter(console_formatter)
# File handler
log_dir = os.path.join(os.path.dirname(__file__), "..", "logs")
os.makedirs(log_dir, exist_ok=True)
file_handler = logging.handlers.RotatingFileHandler(
os.path.join(log_dir, "comfyui_copilot.log"),
maxBytes=10*1024*1024, # 10MB
backupCount=7,
encoding='utf-8'
)
file_handler.setLevel(logging.DEBUG)
# File formatter
file_format = '%(asctime)s | %(levelname)-8s | %(location)s | %(message)s'
file_formatter = LocationFormatter(file_format, datefmt='%Y-%m-%d %H:%M:%S')
file_handler.setFormatter(file_formatter)
# Add handlers to logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)
return logger
class Logger:
"""Logger wrapper that provides convenient logging methods with automatic location detection."""
def __init__(self, name=None):
self._logger = setup_logger()
if name:
self._logger = logging.getLogger(f'comfyui_copilot.{name}')
self._logger.setLevel(logging.DEBUG)
# Prevent propagation to parent logger to avoid duplicate messages
self._logger.propagate = False
# Copy handlers from parent logger if the named logger doesn't have any
if not self._logger.handlers:
parent_logger = logging.getLogger('comfyui_copilot')
for handler in parent_logger.handlers:
self._logger.addHandler(handler)
def _log_with_location(self, level, message, *args, **kwargs):
"""Log message with automatic location detection."""
# Get the caller's frame (2 levels up: _log_with_location -> debug/info/etc -> actual caller)
frame = inspect.currentframe().f_back.f_back
try:
filename = os.path.basename(frame.f_code.co_filename)
function_name = frame.f_code.co_name
line_number = frame.f_lineno
# Create a log record manually to ensure no duplicate processing
if self._logger.isEnabledFor(level):
record = self._logger.makeRecord(
self._logger.name, level, frame.f_code.co_filename, line_number,
message, args, None, function_name
)
record.location = f"{filename}:{function_name}:{line_number}"
# Process the record through handlers directly to avoid duplication
for handler in self._logger.handlers:
if record.levelno >= handler.level:
handler.handle(record)
finally:
del frame
def debug(self, message, *args, **kwargs):
"""Log debug message."""
self._log_with_location(logging.DEBUG, message, *args, **kwargs)
def info(self, message, *args, **kwargs):
"""Log info message."""
self._log_with_location(logging.INFO, message, *args, **kwargs)
def warning(self, message, *args, **kwargs):
"""Log warning message."""
self._log_with_location(logging.WARNING, message, *args, **kwargs)
def warn(self, message, *args, **kwargs):
"""Log warning message (alias for warning)."""
self.warning(message, *args, **kwargs)
def error(self, message, *args, **kwargs):
"""Log error message."""
self._log_with_location(logging.ERROR, message, *args, **kwargs)
def critical(self, message, *args, **kwargs):
"""Log critical message."""
self._log_with_location(logging.CRITICAL, message, *args, **kwargs)
def exception(self, message, *args, **kwargs):
"""Log exception message with traceback."""
# For exceptions, we want to use the standard logger.exception which includes traceback
frame = inspect.currentframe().f_back
try:
filename = os.path.basename(frame.f_code.co_filename)
function_name = frame.f_code.co_name
line_number = frame.f_lineno
# Use the standard exception logging with location info
self._logger.exception(message, *args, **kwargs,
extra={'location': f"{filename}:{function_name}:{line_number}"},
stacklevel=2)
finally:
del frame
# Create default logger instance
log = Logger()
# For backward compatibility and convenience
debug = log.debug
info = log.info
warning = log.warning
warn = log.warn
error = log.error
critical = log.critical
exception = log.exception
# Allow creating named loggers
def get_logger(name=None):
"""Get a logger instance, optionally with a specific name."""
return Logger(name)
__all__ = [
'log', 'Logger', 'get_logger',
'debug', 'info', 'warning', 'warn', 'error', 'critical', 'exception'
]