Spaces:
Sleeping
Sleeping
| """In-memory + file application log so the Logs page can view recent events.""" | |
| from __future__ import annotations | |
| import logging | |
| import threading | |
| from collections import deque | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from typing import List, Optional | |
| from ..database import DATA_DIR | |
| _LOCK = threading.Lock() | |
| _BUFFER: deque = deque(maxlen=4000) | |
| _HANDLER: Optional["RingLogHandler"] = None | |
| LOG_DIR = DATA_DIR / "logs" | |
| LOG_FILE = LOG_DIR / "app.log" | |
| _LEVELS = { | |
| "DEBUG": logging.DEBUG, | |
| "INFO": logging.INFO, | |
| "WARNING": logging.WARNING, | |
| "ERROR": logging.ERROR, | |
| "CRITICAL": logging.CRITICAL, | |
| } | |
| class RingLogHandler(logging.Handler): | |
| def emit(self, record: logging.LogRecord) -> None: | |
| try: | |
| msg = self.format(record) | |
| except Exception: | |
| msg = record.getMessage() | |
| created = datetime.fromtimestamp(record.created, tz=timezone.utc) | |
| entry = { | |
| "ts": created.isoformat(), | |
| "level": record.levelname, | |
| "logger": record.name, | |
| "message": msg, | |
| } | |
| with _LOCK: | |
| _BUFFER.append(entry) | |
| def install_app_log_handler() -> None: | |
| """Attach once to the root logger (and uvicorn access) for the Logs UI.""" | |
| global _HANDLER | |
| if _HANDLER is not None: | |
| return | |
| try: | |
| LOG_DIR.mkdir(parents=True, exist_ok=True) | |
| except OSError: | |
| pass | |
| handler = RingLogHandler(level=logging.INFO) | |
| handler.setFormatter(logging.Formatter("%(message)s")) | |
| _HANDLER = handler | |
| root = logging.getLogger() | |
| root.addHandler(handler) | |
| if root.level > logging.INFO or root.level == logging.NOTSET: | |
| root.setLevel(logging.INFO) | |
| file_handler = None | |
| try: | |
| file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8") | |
| file_handler.setLevel(logging.INFO) | |
| file_handler.setFormatter(logging.Formatter( | |
| "%(asctime)s %(levelname)s [%(name)s] %(message)s" | |
| )) | |
| root.addHandler(file_handler) | |
| except OSError: | |
| file_handler = None | |
| for name in ("uvicorn", "uvicorn.error", "uvicorn.access"): | |
| logging.getLogger(name).addHandler(handler) | |
| if file_handler: | |
| logging.getLogger(name).addHandler(file_handler) | |
| def query_logs( | |
| *, | |
| level: str = "", | |
| q: str = "", | |
| logger_name: str = "", | |
| limit: int = 200, | |
| offset: int = 0, | |
| ) -> dict: | |
| min_level = _LEVELS.get((level or "").upper(), 0) | |
| needle = (q or "").strip().lower() | |
| logger_f = (logger_name or "").strip().lower() | |
| with _LOCK: | |
| rows = list(_BUFFER) | |
| rows.reverse() | |
| filtered: List[dict] = [] | |
| for row in rows: | |
| if min_level and _LEVELS.get(row["level"], 0) < min_level: | |
| continue | |
| if logger_f and logger_f not in (row.get("logger") or "").lower(): | |
| continue | |
| if needle and needle not in (row.get("message") or "").lower() and needle not in (row.get("logger") or "").lower(): | |
| continue | |
| filtered.append(row) | |
| total = len(filtered) | |
| page = filtered[offset:offset + limit] | |
| return { | |
| "total": total, | |
| "offset": offset, | |
| "limit": limit, | |
| "logs": page, | |
| "file": str(LOG_FILE) if LOG_FILE.exists() else "", | |
| } | |
| def clear_logs() -> dict: | |
| with _LOCK: | |
| _BUFFER.clear() | |
| truncated = False | |
| try: | |
| LOG_DIR.mkdir(parents=True, exist_ok=True) | |
| LOG_FILE.write_text("", encoding="utf-8") | |
| truncated = True | |
| except OSError: | |
| pass | |
| return {"ok": True, "cleared": True, "fileTruncated": truncated} | |
| def log_file_path() -> Path: | |
| return LOG_FILE | |