Files
olivv-cs-ComfyUI-FunPack/core/log.py
T
digital-garbageandClaude Opus 5 74671329f9 log: a severity, a source, and a message
[Log] Info: FunPack Diffusion Model Loader | flux.safetensors loaded, attention sage
    [Log] Alert: MiniMaxH3ImageToVideo node | no "first_image" input is connected
    [Log] Warning: FunPack LoRA Loader | style.safetensors is empty and will not load
    [Log] Critical: torch | cannot allocate 14 GB (3 GB free); the run will not start

The difference between the middle two is the one that matters. Alert is a
choice you may not have made on purpose. Warning is something that will NOT
do what its name says -- the level v4 needed and did not have, which is how
560 handlers came to swallow a failure and carry on, so a feature could be
switched on, do nothing, and never say so.

Info is not decoration either: "loaded on GPU using sage attention" is how
an attention setting that did nothing gets noticed months before the
pictures look wrong enough to investigate.

Every call site moved -- there is no note() left to be vague through -- and
the loaders now say what they actually loaded rather than only what they
were asked for. Records are kept in a bounded ring as well as printed, so a
long rental session cannot turn the log into a memory leak, and served at
/funpack/api/log so a person can read it somewhere other than a terminal
full of ComfyUI's own output. There is a page for it.

Also dropped the preflight this was traded for: min and max are already
declared and already enforced at both doors, and a second protection layer
around a check that exists is ceremony.

Two mistakes worth keeping: a log line reached into the model's shape to
describe it and could have failed the load it was only narrating; and both
new pages passed `children` to an element that takes `rows`, which the kit
accepted and silently dropped. It throws on an unknown element and not on an
unknown prop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 17:09:33 +03:00

111 lines
3.7 KiB
Python

"""Saying what happened, at a severity, from somewhere.
[Log] Info: FunPack Diffusion Model Loader | flux.safetensors loaded, attention sage
[Log] Alert: FunPack Sampler | no image is connected, starting from noise
[Log] Warning: FunPack LoRA Loader | style.safetensors is empty and will not load
[Log] Critical: torch | cannot allocate 14 GB (3 GB free); the run will not start
Four levels, and the difference between the middle two is the one that matters:
* **Info** -- it worked, and here is what it actually did. Not decoration: "loaded
on GPU using sage attention" is how you find out the attention setting did
nothing, months before the pictures look wrong enough to investigate.
* **Alert** -- something is unset or unconnected and the run continues without
it. A choice you may not have made on purpose.
* **Warning** -- something will NOT do what its name says. This is the level v4
needed and did not have: 560 handlers swallowed a failure and carried on, so a
feature could be switched on, do nothing, and never say so.
* **Critical** -- the run cannot proceed.
Records are kept in a small ring as well as printed, so the app can show them.
The buffer is bounded because a long rental session must not turn a log into a
memory leak.
"""
import sys
import time
from collections import deque
from typing import Optional
LABEL = "[Log]"
INFO = "Info"
ALERT = "Alert"
WARNING = "Warning"
CRITICAL = "Critical"
LEVELS = (INFO, ALERT, WARNING, CRITICAL)
# Enough to cover a long generation and its aftermath, small enough to be free.
HISTORY = 500
_records: deque = deque(maxlen=HISTORY)
# Keys already said once. Cleared per run rather than per process: v4's dedup
# lasted the life of the interpreter, so a session reported the first generation
# that went inert and stayed quiet for every one after it.
_said: set = set()
def line(level: str, source: str, message: str) -> str:
"""The one place the format is decided."""
where = f"{source} | " if source else ""
return f"{LABEL} {level}: {where}{message}"
def record(level: str, source: str, message: str) -> dict:
entry = {"at": time.time(), "level": level, "source": source, "message": message}
_records.append(entry)
print(line(level, source, message), file=sys.stderr, flush=True)
return entry
def info(source: str, message: str) -> dict:
return record(INFO, source, message)
def alert(source: str, message: str) -> dict:
return record(ALERT, source, message)
def warning(source: str, message: str) -> dict:
return record(WARNING, source, message)
def critical(source: str, message: str) -> dict:
return record(CRITICAL, source, message)
def once(key: str, level: str, source: str, message: str) -> Optional[dict]:
"""Say it the first time only, until the next run.
For anything that would otherwise repeat every sampling step. Thirty copies
of one line say nothing the first did not, and burying the console is its own
way of hiding a failure.
"""
if key in _said:
return None
_said.add(key)
return record(level, source, message)
def failed(what: str, exc: BaseException) -> dict:
"""Something did not load. Always a Warning: it means a feature is absent."""
return warning(what, f"did not load -- {type(exc).__name__}: {exc}")
def new_run() -> None:
"""A generation is starting: whatever was said once may be said again."""
_said.clear()
def history(level: Optional[str] = None, limit: int = HISTORY) -> list:
"""Recent records, newest last. For the app, and for the modules page."""
items = [r for r in _records if level is None or r["level"] == level]
return items[-limit:]
def _reset() -> None:
"""Tests only."""
_records.clear()
_said.clear()