"""Redis-backed live render log for streaming task progress. Each order line gets a Redis list keyed by render:log:{order_line_id}. Entries are JSON objects with timestamp, level, and message. Lists auto-expire after 1 hour. """ import json import time import logging import redis from app.config import settings logger = logging.getLogger(__name__) _LOG_TTL = 3600 # 1 hour _MAX_ENTRIES = 500 def _redis() -> redis.Redis: return redis.from_url(settings.redis_url, decode_responses=True) def _key(order_line_id: str) -> str: return f"render:log:{order_line_id}" def emit(order_line_id: str, message: str, level: str = "info") -> None: """Push a log entry for a render job.""" entry = json.dumps({ "ts": time.time(), "t": time.strftime("%H:%M:%S", time.gmtime()), "level": level, "msg": message, }) try: r = _redis() key = _key(order_line_id) r.rpush(key, entry) r.ltrim(key, -_MAX_ENTRIES, -1) r.expire(key, _LOG_TTL) except Exception as exc: logger.debug(f"render_log emit failed: {exc}") def get_entries(order_line_id: str, after_index: int = 0) -> list[dict]: """Get log entries starting from after_index.""" try: r = _redis() raw = r.lrange(_key(order_line_id), after_index, -1) return [json.loads(e) for e in raw] except Exception: return [] def count(order_line_id: str) -> int: """Get the number of log entries.""" try: r = _redis() return r.llen(_key(order_line_id)) except Exception: return 0 def clear(order_line_id: str) -> None: """Clear log entries for a render job.""" try: r = _redis() r.delete(_key(order_line_id)) except Exception: pass