Files
ttulttul-ComfyUI-Modal/cloud_runtime_logging.py
T
Ken SimpsonandClaude Opus 5.5 cbdaf4a5c7 Emit INFO logs from all project modules inside the Modal container
Only the comfyui_modal_sync_cloud entrypoint logger had a handler in the
container. Flat-imported sibling modules (cloud_durable_invocation,
cloud_session_bridge, ...) fell through to the root WARNING handler, so
their INFO timing lines were dropped. That hid the durable-result step that
accounted for 28-90s per-item delays after execution finished.

Inside the container, give every shipped top-level module an INFO handler
that prints to the live sys.stdout at emit time (snapshot restores can swap
stdout), with propagation off so warnings are not printed twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:01:19 -07:00

165 lines
5.6 KiB
Python

"""Timestamped logging and phase timing for the cloud worker runtime."""
from __future__ import annotations
from contextlib import contextmanager
import logging
import os
from pathlib import Path
import sys
import time
from typing import Any, Iterable, Iterator
logger = logging.getLogger(__name__)
_TARGET_LOGGER: logging.Logger = logger
_HANDLER_NAME = "comfyui-modal-sync-cloud-timestamped"
_MODULE_HANDLER_NAME = "comfyui-modal-sync-cloud-modules"
def _build_cloud_log_formatter() -> logging.Formatter:
"""Return the default formatter for remote Modal-Sync logs with timestamps."""
return logging.Formatter(
fmt="%(asctime)s.%(msecs)03d +%(relativeCreated)07.0fms %(levelname)s [%(name)s] %(message)s",
datefmt="%H:%M:%S",
)
def configure_cloud_runtime_logging(
target_logger: logging.Logger,
handler_name: str,
) -> logging.Logger:
"""Configure and retain the stable entrypoint logger used by cloud helpers."""
global _TARGET_LOGGER
global _HANDLER_NAME
_TARGET_LOGGER = target_logger
_HANDLER_NAME = handler_name
target_logger.setLevel(logging.INFO)
for existing_handler in target_logger.handlers:
if getattr(existing_handler, "name", "") == handler_name:
return target_logger
handler = logging.StreamHandler(sys.stdout)
handler.set_name(handler_name)
handler.setLevel(logging.INFO)
handler.setFormatter(_build_cloud_log_formatter())
target_logger.addHandler(handler)
target_logger.propagate = False
return target_logger
class _CurrentStdoutHandler(logging.Handler):
"""Write each record to whatever ``sys.stdout`` is when the record is emitted.
Modal memory-snapshot restores can replace ``sys.stdout`` after import, so a
stream captured at import time may no longer reach the container log.
"""
def emit(self, record: logging.LogRecord) -> None:
"""Format one record and print it to the live stdout stream."""
try:
print(self.format(record), file=sys.stdout, flush=True)
except (OSError, ValueError):
self.handleError(record)
def cloud_project_module_names(repo_root: Path) -> tuple[str, ...]:
"""Return the flat module names of the project files shipped to the container."""
return tuple(
sorted(
module_path.stem
for module_path in repo_root.glob("*.py")
if module_path.stem != "__init__"
)
)
def _cloud_module_handler(
module_loggers: Iterable[logging.Logger],
) -> logging.Handler:
"""Return the shared module handler, reusing one already installed."""
for module_logger in module_loggers:
for existing_handler in module_logger.handlers:
if getattr(existing_handler, "name", "") == _MODULE_HANDLER_NAME:
return existing_handler
handler = _CurrentStdoutHandler(level=logging.INFO)
handler.set_name(_MODULE_HANDLER_NAME)
handler.setFormatter(_cloud_formatter())
return handler
def configure_cloud_module_loggers(module_names: Iterable[str]) -> tuple[str, ...]:
"""Emit INFO logs from sibling project modules inside the Modal container.
Only the entrypoint logger used to get a handler, so INFO records from modules
such as ``cloud_durable_invocation`` fell through to Python's WARNING-only
fallback and were silently dropped.
"""
module_loggers = [
logging.getLogger(module_name)
for module_name in module_names
if module_name != _TARGET_LOGGER.name
]
handler = _cloud_module_handler(module_loggers)
for module_logger in module_loggers:
module_logger.setLevel(logging.INFO)
if handler not in module_logger.handlers:
module_logger.addHandler(handler)
module_logger.propagate = False
configured_names = tuple(module_logger.name for module_logger in module_loggers)
logger.debug("Configured cloud module loggers: %s", configured_names)
return configured_names
def _is_modal_container_runtime() -> bool:
"""Return whether the current process is executing inside a Modal container."""
return os.getenv("MODAL_IS_REMOTE") == "1" or bool(os.getenv("MODAL_TASK_ID"))
def _cloud_formatter() -> logging.Formatter:
"""Return the configured formatter used for cloud phase trace lines."""
for existing_handler in _TARGET_LOGGER.handlers:
if getattr(existing_handler, "name", "") == _HANDLER_NAME:
formatter = existing_handler.formatter
if formatter is not None:
return formatter
return _build_cloud_log_formatter()
def _emit_cloud_info(message: str, *args: Any) -> None:
"""Emit an info line and mirror it to stdout inside Modal containers."""
if not _is_modal_container_runtime():
_TARGET_LOGGER.info(message, *args)
return
record = _TARGET_LOGGER.makeRecord(
_TARGET_LOGGER.name,
logging.INFO,
__file__,
0,
message,
args,
exc_info=None,
)
print(_cloud_formatter().format(record), file=sys.stdout, flush=True)
@contextmanager
def _timed_phase(phase: str, **fields: Any) -> Iterator[None]:
"""Log a start/finish pair with elapsed time for a named execution phase."""
field_suffix = ""
if fields:
rendered_fields = " ".join(f"{key}={value}" for key, value in fields.items())
field_suffix = f" {rendered_fields}"
phase_started_at = time.perf_counter()
_emit_cloud_info("Starting %s%s", phase, field_suffix)
try:
yield
finally:
_emit_cloud_info(
"Finished %s in %.3fs%s",
phase,
time.perf_counter() - phase_started_at,
field_suffix,
)