Phase 1 of logging work.

This commit is contained in:
Shanoah Alkire
2026-02-22 08:07:45 -08:00
parent cb5ca179a9
commit 2990b74a83
7 changed files with 326 additions and 40 deletions
+21
View File
@@ -205,6 +205,27 @@ Example workflows are available in the `example_workflows/` folder. In ComfyUI,
The sidebar tabs (LLM Chat, Prompt Builder) will appear automatically in the ComfyUI interface.
## Logging Configuration
SageUtils uses a dedicated logging system that is separate from ComfyUI's logging. You can control log verbosity using the `SAGEUTILS_LOG_LEVEL` environment variable:
```bash
# Show detailed debug information
SAGEUTILS_LOG_LEVEL=DEBUG python main.py
# Show only warnings and errors (quieter)
SAGEUTILS_LOG_LEVEL=WARNING python main.py
# Default level is INFO (normal operation messages)
python main.py
```
Available log levels: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
All SageUtils logs are prefixed with `[SageUtils.*]` for easy identification.
For developers, see the [Logging Guide](docs/LOGGING.md) for information on using the logger in your code.
## Documentation
- 📖 [LLM Tab Guide](docs/LLM_TAB_GUIDE.md) - Complete guide to using the LLM chat interface
+12 -6
View File
@@ -1,8 +1,15 @@
import os
import logging
# Uncomment the next line to enable debug logging
# logging.basicConfig(level=logging.DEBUG)
# Initialize SageUtils logging infrastructure
from .utils.logger import configure_logging, configure_third_party_logging, get_logger
# Configure SageUtils logger early in initialization
# Uses SAGEUTILS_LOG_LEVEL environment variable or defaults to INFO
configure_logging()
configure_third_party_logging()
# Get logger for this module
logger = get_logger('init')
SAGEUTILS_PRINT_TIMING = False # Set to True to enable timing report
# Print timing report if enabled via environment variable
@@ -55,8 +62,7 @@ try:
from . import server_routes
log_init("SERVER_ROUTES_LOADED")
except Exception as e:
import logging
logging.warning(f"Warning: Failed to load SageUtils custom routes: {e}")
logger.warning(f"Failed to load SageUtils custom routes: {e}")
log_init("SERVER_ROUTES_FAILED")
WEB_DIRECTORY = "./js"
@@ -70,7 +76,7 @@ try:
from .utils.performance_fix import populate_llm_cache_async
populate_llm_cache_async()
except Exception as e:
logging.warning(f"Warning: Failed to start background LLM cache population: {e}")
logger.warning(f"Failed to start background LLM cache population: {e}")
# Print timing report if enabled.
if SAGEUTILS_PRINT_TIMING:
+95 -16
View File
@@ -212,10 +212,22 @@ class Sage_FlexibleCLIPSelector(io.ComfyNode):
io.Combo.Input(f"clip_name_{i}", display_name=f"clip_name_{i}", options=clip_list)
)
# Add clip_type selector
default_type = "chroma" if num_clips == 1 else "sdxl"
clip_options[num_clips].append(
io.Combo.Input("clip_type", display_name="clip_type", options=mi.single_clip_loader_options, default=default_type)
)
options = None
if num_clips == 1:
options = mi.single_clip_loader_options
default_type = "chroma"
elif num_clips == 2:
options = mi.dual_clip_loader_options
default_type = "sdxl"
else:
options = None
default_type = ""
if options:
clip_options[num_clips].append(
io.Combo.Input("clip_type", display_name="clip_type", options=options, default=default_type)
)
# Build dynamic combo options
dynamic_options = [
@@ -250,6 +262,78 @@ class Sage_FlexibleCLIPSelector(io.ComfyNode):
info = mi.get_model_info_clips(clip_names, clip_type)
return io.NodeOutput(info)
class Sage_MultiSelectorFlexibleClip(io.ComfyNode):
@classmethod
def define_schema(cls):
unet_options = get_model_list("unet")
vae_options = get_model_list("vae")
clip_list = get_model_list("clip")
clip_options = {}
# Build clip options for 1-4 clips
for num_clips in range(1, 5):
clip_options[num_clips] = []
for i in range(1, num_clips + 1):
clip_options[num_clips].append(
io.Combo.Input(f"clip_name_{i}", display_name=f"clip_name_{i}", options=clip_list)
)
# Add clip_type selector
options = None
if num_clips == 1:
options = mi.single_clip_loader_options
default_type = "chroma"
elif num_clips == 2:
options = mi.dual_clip_loader_options
default_type = "sdxl"
else:
options = None
default_type = ""
if options:
clip_options[num_clips].append(
io.Combo.Input("clip_type", display_name="clip_type", options=options, default=default_type)
)
# Build dynamic combo options
dynamic_options = [
io.DynamicCombo.Option(str(num), list(clip_options[num]))
for num in range(1, 5)
]
return io.Schema(
node_id="Sage_MultiSelectorFlexibleClip",
display_name="Multi Selector Flexible CLIP",
description="Selects checkpoint, UNET, VAE, and a flexible number of CLIP models from lists.",
category="Sage Utils/selector",
inputs=[
io.Combo.Input("unet_name", display_name="unet_name", options=unet_options),
io.Combo.Input("weight_dtype", display_name="weight_dtype", options=mi.weight_dtype_options, default="default"),
io.DynamicCombo.Input("num_of_clips", display_name="num_of_clips", options=dynamic_options),
io.Combo.Input("vae_name", display_name="vae_name", options=vae_options),
],
outputs=[
ModelInfo.Output("model_info", display_name="model_info")
]
)
@classmethod
def execute(cls, **kwargs):
unet_name = kwargs.get("unet_name", "")
weight_dtype = kwargs.get("weight_dtype", "default")
vae_name = kwargs.get("vae_name", "")
args = kwargs.get("num_of_clips", {})
clip_names = [args.get(key, "") for key in sorted(args.keys()) if key.startswith("clip_name_")]
clip_type = kwargs.get("clip_type", "chroma")
unet_info = mi.get_model_info_unet(unet_name, weight_dtype)
clip_info = mi.get_model_info_clips(clip_names, clip_type)
vae_info = mi.get_model_info_vae(vae_name)
ret = (unet_info[0], clip_info[0], vae_info[0])
return io.NodeOutput(ret,)
class Sage_MultiSelectorSingleClip(io.ComfyNode):
@classmethod
def define_schema(cls):
@@ -311,13 +395,12 @@ class Sage_MultiSelectorDoubleClip(io.ComfyNode):
def execute(cls, **kwargs):
unet_name = kwargs.get("unet_name", "")
weight_dtype = kwargs.get("weight_dtype", "default")
clip_name_1 = kwargs.get("clip_name_1", "")
clip_name_2 = kwargs.get("clip_name_2", "")
clip_name = [kwargs.get("clip_name_1", ""), kwargs.get("clip_name_2", "")]
clip_type = kwargs.get("clip_type", "sdxl")
vae_name = kwargs.get("vae_name", "")
unet_info = mi.get_model_info_unet(unet_name, weight_dtype)
clip_info = mi.get_model_info_clips([clip_name_1, clip_name_2], clip_type)
clip_info = mi.get_model_info_clips(clip_name, clip_type)
vae_info = mi.get_model_info_vae(vae_name)
ret = (unet_info[0], clip_info[0], vae_info[0])
@@ -349,13 +432,11 @@ class Sage_MultiSelectorTripleClip(io.ComfyNode):
def execute(cls, **kwargs):
unet_name = kwargs.get("unet_name", "")
weight_dtype = kwargs.get("weight_dtype", "default")
clip_name_1 = kwargs.get("clip_name_1", "")
clip_name_2 = kwargs.get("clip_name_2", "")
clip_name_3 = kwargs.get("clip_name_3", "")
clip_name = [kwargs.get("clip_name_1", ""), kwargs.get("clip_name_2", ""), kwargs.get("clip_name_3", "")]
vae_name = kwargs.get("vae_name", "")
unet_info = mi.get_model_info_unet(unet_name, weight_dtype)
clip_info = mi.get_model_info_clips([clip_name_1, clip_name_2, clip_name_3])
clip_info = mi.get_model_info_clips(clip_name)
vae_info = mi.get_model_info_vae(vae_name)
ret = (unet_info[0], clip_info[0], vae_info[0])
@@ -388,14 +469,11 @@ class Sage_MultiSelectorQuadClip(io.ComfyNode):
def execute(cls, **kwargs):
unet_name = kwargs.get("unet_name", "")
weight_dtype = kwargs.get("weight_dtype", "default")
clip_name_1 = kwargs.get("clip_name_1", "")
clip_name_2 = kwargs.get("clip_name_2", "")
clip_name_3 = kwargs.get("clip_name_3", "")
clip_name_4 = kwargs.get("clip_name_4", "")
clip_name = [kwargs.get("clip_name_1", ""), kwargs.get("clip_name_2", ""), kwargs.get("clip_name_3", ""), kwargs.get("clip_name_4", "")]
vae_name = kwargs.get("vae_name", "")
unet_info = mi.get_model_info_unet(unet_name, weight_dtype)
clip_info = mi.get_model_info_clips([clip_name_1, clip_name_2, clip_name_3, clip_name_4])
clip_info = mi.get_model_info_clips(clip_name)
vae_info = mi.get_model_info_vae(vae_name)
ret = (unet_info[0], clip_info[0], vae_info[0])
@@ -946,6 +1024,7 @@ SELECTOR_NODES = [
Sage_MultiSelectorTripleClip,
Sage_MultiSelectorQuadClip,
Sage_FlexibleCLIPSelector,
Sage_MultiSelectorFlexibleClip,
Sage_ModelShifts,
Sage_ModelShiftOnly,
Sage_FreeU2,
+8 -16
View File
@@ -334,7 +334,7 @@ def pull_metadata(file_paths, timestamp = True, force_all = False, pbar = None,
force = force_all
hash = cache.hash.get(str(file_path), None)
if hash is None:
print(f"Hash not found in cache for {file_path}. Adding to cache.")
logging.debug(f"Hash not found in cache for {file_path}. Adding to cache.")
hash = add_file_to_cache(file_path)
file_cache = cache.by_path(file_path)
@@ -344,7 +344,7 @@ def pull_metadata(file_paths, timestamp = True, force_all = False, pbar = None,
modified = get_file_modification_date(file_path)
# If file was modified after last used, force metadata pull
if last_used_date is not None and modified is not None and modified > last_used_date:
print(f"File was modified after last used. Pulling metadata.")
logging.info(f"File was modified after last used. Pulling metadata.")
force = True
# Only skip pull if not forced and civitai is True and recently pulled
@@ -361,16 +361,16 @@ def pull_metadata(file_paths, timestamp = True, force_all = False, pbar = None,
if file_cache.get('blacklist'):
if not force:
print(f"File {file_path} is blacklisted (previously not found). Skipping metadata pull.")
logging.warning(f"File {file_path} is blacklisted (previously not found). Skipping metadata pull.")
pull_json = False
# If force, recalculate hash before any API call
if force:
print(f"Force flag is set. Recalculating hash for {file_path}.")
logging.debug(f"Force flag is set. Recalculating hash for {file_path}.")
hash = recheck_hash(file_path, hash)
if pull_json or force:
print(f"Currently pulling metadata for {file_path}.")
logging.debug(f"Currently pulling metadata for {file_path}.")
json = get_civitai_model_version_json_by_hash(hash)
if 'error' in json:
@@ -383,15 +383,15 @@ def pull_metadata(file_paths, timestamp = True, force_all = False, pbar = None,
# Try fallback with modelId if available
if dead_model is False:
if 'modelId' in file_cache:
print(f"Using cached model id {file_cache.get('id', None)}")
logging.debug(f"Using cached model id {file_cache.get('id', None)}")
json = get_civitai_model_version_json_by_id(file_cache['id'])
retried = True
else:
print(f"No cached model id.")
logging.debug(f"No cached model id.")
if 'error' in json:
if retried:
print(f"Error: {json['error']}")
logging.error(f"Error: {json['error']}")
if dead_model:
file_cache['blacklist'] = True
print(f"Unable to find on civitai.")
@@ -669,14 +669,6 @@ def clean_keywords(keywords):
return ', '.join(keywords)
def clean_text(text):
# ret = ' '.join(filter(None, (x.strip() for x in text.split())))
# ret = ', '.join(filter(None, (x.strip() for x in ret.split(','))))
# ret = '\n'.join(filter(None, (x.strip() for x in ret.split('\n'))))
# # Strip whitespace from the start and end of text in parentheses
# ret = ' ('.join(part.strip() for part in ret.split('('))
# ret = ')'.join(part.strip() for part in ret.split(')'))
ret = normalize_prompt_weights(text)
return ret
+3
View File
@@ -6,6 +6,8 @@ from .llm_cache import get_llm_cache
_ollama_initialized = False
_lmstudio_initialized = False
logging.getLogger('httpx').setLevel(logging.WARNING)
# Attempt to import ollama, if available. Set a flag if it is not available.
try:
import ollama
@@ -13,6 +15,7 @@ try:
ollama_client = None # Will be initialized in init_ollama
ollama_logger = logging.getLogger('ollama')
ollama_logger.setLevel(logging.ERROR)
except ImportError:
ollama = None
+185
View File
@@ -0,0 +1,185 @@
"""
SageUtils logging infrastructure.
Provides a dedicated logger for all SageUtils operations.
"""
import logging
import os
from typing import Optional
# Logger name for all SageUtils components
LOGGER_NAME = "SageUtils"
# Default log level (can be overridden via environment variable)
DEFAULT_LOG_LEVEL = logging.INFO
# Environment variable for log level control
LOG_LEVEL_ENV_VAR = "SAGEUTILS_LOG_LEVEL"
# Valid log level names
LOG_LEVELS = {
"DEBUG": logging.DEBUG,
"INFO": logging.INFO,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
"CRITICAL": logging.CRITICAL,
}
def get_logger(name: Optional[str] = None) -> logging.Logger:
"""
Get a logger instance for SageUtils.
Args:
name: Optional sub-module name (e.g., 'cache', 'routes', 'helpers')
If provided, creates logger as 'SageUtils.{name}'
If None, returns the root SageUtils logger
Returns:
Logger instance configured for SageUtils
Example:
# In utils/helpers.py
logger = get_logger('helpers')
logger.info("Processing model...") # Logs as [SageUtils.helpers]
# In routes/cache_routes.py
logger = get_logger('routes.cache')
logger.debug("Cache route called") # Logs as [SageUtils.routes.cache]
"""
if name:
logger_name = f"{LOGGER_NAME}.{name}"
else:
logger_name = LOGGER_NAME
return logging.getLogger(logger_name)
def configure_logging(level: Optional[int] = None, handler: Optional[logging.Handler] = None):
"""
Configure the SageUtils logger with desired level and handler.
This should be called once during initialization (__init__.py).
Args:
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
If None, reads from SAGEUTILS_LOG_LEVEL env var or uses DEFAULT_LOG_LEVEL
handler: Custom handler to use. If None, uses StreamHandler with standard formatting
Notes:
- Only affects SageUtils logger, not ComfyUI's logging
- Sub-loggers (e.g., SageUtils.helpers) inherit this configuration
- Third-party library loggers can be configured separately
Returns:
The configured root SageUtils logger
"""
# Get or create the root SageUtils logger
logger = logging.getLogger(LOGGER_NAME)
# Determine log level
if level is None:
# Check environment variable
env_level = os.environ.get(LOG_LEVEL_ENV_VAR, "").upper()
level = LOG_LEVELS.get(env_level, DEFAULT_LOG_LEVEL)
logger.setLevel(level)
# Clear existing handlers to avoid duplicates
logger.handlers.clear()
# Add handler
if handler is None:
handler = logging.StreamHandler()
formatter = logging.Formatter(
'[%(name)s] %(levelname)s: %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
# Prevent propagation to root logger to avoid duplicate messages
logger.propagate = False
return logger
def configure_third_party_logging():
"""
Configure logging levels for third-party libraries used by SageUtils.
Some libraries are very verbose and should have higher logging thresholds.
This function sets appropriate levels for known chatty libraries.
"""
# HTTP libraries
logging.getLogger('httpx').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
# LLM libraries (if available)
try:
logging.getLogger('ollama').setLevel(logging.ERROR)
except:
pass
try:
logging.getLogger('lmstudio').setLevel(logging.ERROR)
except:
pass
# Add more as needed
logging.getLogger('asyncio').setLevel(logging.WARNING)
def get_sageutils_logger() -> logging.Logger:
"""
Get the root SageUtils logger.
This is a convenience function for getting the main logger.
For module-specific loggers, use get_logger(name) instead.
Returns:
The root SageUtils logger
"""
return logging.getLogger(LOGGER_NAME)
def set_log_level(level: str):
"""
Dynamically change the log level for SageUtils logger.
Args:
level: Log level name ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
Raises:
ValueError: If level is not a valid log level name
Example:
set_log_level('DEBUG') # Enable debug logging
set_log_level('WARNING') # Only show warnings and errors
"""
level_upper = level.upper()
if level_upper not in LOG_LEVELS:
raise ValueError(
f"Invalid log level: {level}. "
f"Valid levels are: {', '.join(LOG_LEVELS.keys())}"
)
logger = logging.getLogger(LOGGER_NAME)
logger.setLevel(LOG_LEVELS[level_upper])
def get_log_level() -> str:
"""
Get the current log level name for SageUtils logger.
Returns:
Log level name ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
"""
logger = logging.getLogger(LOGGER_NAME)
level = logger.level
# Reverse lookup
for name, value in LOG_LEVELS.items():
if value == level:
return name
return "UNKNOWN"
+2 -2
View File
@@ -7,8 +7,8 @@ from .model_cache import cache
from .lora_stack import norm_lora_stack
weight_dtype_options = ["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"]
single_clip_loader_options = ["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos", "lumina2", "wan", "hidream", "chroma", "ace", "omnigen2", "qwen_image"]
dual_clip_loader_options = ["sdxl", "sd3", "flux", "hunyuan_video", "hidream", "ace"]
single_clip_loader_options = ["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos", "lumina2", "wan", "hidream", "chroma", "ace", "omnigen2", "qwen_image", "hunyuan_image", "flux2", "ovis"]
dual_clip_loader_options = ["sdxl", "sd3", "flux", "hunyuan_video", "hidream", "hunyuan_image", "hunyuan_video_15", "kandinsky5", "kandinsky5_image", "ltxv", "newbie", "ace"]
def get_model_info_ckpt(ckpt_name: str) -> tuple:
"""