hue-portal-backend / monitor_space.py
Davidtran99
chore: sync with main repo
3718c84
#!/usr/bin/env python3
"""
Utility script to monitor Hugging Face Space status + logs.
Features:
- Fetch latest build/runtime info using huggingface_hub
- Download logs via HF REST API and persist to logs/hf_space/
- Highlight errors (Traceback/SyntaxError) directly in console
- Optional --watch mode to poll every N seconds
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional
import requests
from huggingface_hub import HfApi
DEFAULT_SPACE_ID = "davidtran999/hue-portal-backend"
LOG_ROOT = Path(__file__).resolve().parent / "logs" / "hf_space"
def get_hf_token() -> str:
"""Return Hugging Face token from env or cached file."""
token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_HUB_TOKEN")
if token:
return token.strip()
cache_file = Path.home() / ".cache" / "huggingface" / "token"
if cache_file.exists():
return cache_file.read_text(encoding="utf-8").strip()
return ""
def fetch_space_logs(space_id: str, token: str, limit: int) -> str:
"""Fetch the latest logs, trying both repo path and subdomain API."""
candidates = []
if "/" in space_id:
owner, name = space_id.split("/", 1)
candidates.append(f"https://huggingface.co/api/spaces/{owner}/{name}/logs?limit={limit}")
subdomain = f"{owner}-{name}"
else:
subdomain = space_id
candidates.append(f"https://huggingface.co/api/spaces/{subdomain}/logs?limit={limit}")
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
last_error = None
for url in candidates:
try:
response = requests.get(url, headers=headers, timeout=30)
if response.status_code == 401:
raise RuntimeError("Không có quyền đọc logs. Hãy đặt HF_TOKEN với quyền write.")
if response.status_code == 404:
last_error = f"404 for {url}"
continue
response.raise_for_status()
data = response.json()
logs = data.get("logs") or data.get("log")
if isinstance(logs, list):
return "\n".join(logs)
if isinstance(logs, str):
return logs
return json.dumps(data, ensure_ascii=False)
except Exception as exc:
last_error = str(exc)
continue
raise RuntimeError(f"Không thể lấy logs. Nguyên nhân gần nhất: {last_error}")
def write_log(space_id: str, space_info: Dict[str, Any], logs: str) -> Path:
"""Persist logs + status info to disk."""
LOG_ROOT.mkdir(parents=True, exist_ok=True)
timestamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
safe_space = space_id.replace("/", "__")
log_path = LOG_ROOT / f"{safe_space}-{timestamp}.log"
with log_path.open("w", encoding="utf-8") as fp:
fp.write(f"# Space: {space_id}\n")
fp.write(f"# Timestamp: {timestamp} UTC\n")
fp.write(f"# Runtime: {space_info.get('runtime', {}).get('stage')}\n")
fp.write(f"# Hardware: {space_info.get('runtime', {}).get('hardware', 'unknown')}\n")
fp.write("# --- Logs ---\n")
fp.write(logs)
return log_path
def monitor(space_id: str, watch: bool, interval: int, limit: int) -> None:
token = get_hf_token()
if not token:
print("⚠️ Không tìm thấy HF token – chỉ có thể đọc log public.")
api = HfApi(token=token or None)
def _single_cycle() -> None:
info = api.space_info(space_id)
logs = fetch_space_logs(space_id, token, limit)
log_path = write_log(space_id, info, logs)
runtime = info.runtime if hasattr(info, "runtime") else getattr(info, "runtime", {})
stage = (runtime or {}).get("stage") if isinstance(runtime, dict) else runtime
hardware = (runtime or {}).get("hardware") if isinstance(runtime, dict) else "unknown"
print(f"\n📡 Space: {space_id}")
print(f" Stage: {stage}, Hardware: {hardware}")
print(f" Updated: {datetime.utcnow().isoformat()}Z")
print(f" Logs saved to: {log_path}")
alert_keywords = ["Traceback", "SyntaxError", "ModuleNotFoundError"]
if any(keyword in logs for keyword in alert_keywords):
print(" 🚨 Detected errors in log (Traceback/SyntaxError). Check file above.")
else:
print(" ✅ No critical errors detected in latest log.")
_single_cycle()
while watch:
time.sleep(interval)
_single_cycle()
def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Monitor Hugging Face Space build/logs.")
parser.add_argument("--space-id", default=DEFAULT_SPACE_ID, help="Ví dụ: owner/space-name")
parser.add_argument("--interval", type=int, default=30, help="Số giây giữa các lần kiểm tra (watch mode)")
parser.add_argument("--limit", type=int, default=200, help="Số dòng log lấy về (max 400)")
parser.add_argument("--watch", action="store_true", help="Bật chế độ theo dõi liên tục")
return parser.parse_args(argv)
def main() -> None:
args = parse_args()
try:
monitor(space_id=args.space_id, watch=args.watch, interval=args.interval, limit=args.limit)
except KeyboardInterrupt:
print("\n⏹️ Dừng theo dõi theo yêu cầu người dùng.")
if __name__ == "__main__":
main()