Fix hub system logs by resolving PM2 log paths from jlist and glob fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-09 10:38:24 +08:00
parent 5214794f95
commit d092f213a7
2 changed files with 150 additions and 18 deletions
+88 -5
View File
@@ -1,7 +1,9 @@
"""中控:读取 PM2 进程 stdout/stderr 日志(系统日志页)."""
from __future__ import annotations
import json
import os
import subprocess
import time
from pathlib import Path
from typing import Any
@@ -16,6 +18,10 @@ LOG_TARGETS: dict[str, dict[str, str]] = {
DEFAULT_LINES = 200
MAX_LINES = 500
DEFAULT_TAIL_BYTES = 400_000
_PATH_CACHE_TTL_SEC = 30
_path_cache: dict[str, tuple[Path, Path]] = {}
_path_cache_at = 0.0
def pm2_logs_dir() -> Path:
@@ -24,9 +30,86 @@ def pm2_logs_dir() -> Path:
return base / "logs"
def log_file_paths(pm2_name: str) -> tuple[Path, Path]:
def _pm2_jlist() -> list[dict[str, Any]]:
try:
proc = subprocess.run(
["pm2", "jlist"],
capture_output=True,
text=True,
timeout=8,
check=False,
)
if proc.returncode != 0:
return []
data = json.loads(proc.stdout or "[]")
return data if isinstance(data, list) else []
except (OSError, subprocess.SubprocessError, json.JSONDecodeError, ValueError):
return []
def _newest_match(pattern: str, logs_dir: Path) -> Path | None:
matches = [p for p in logs_dir.glob(pattern) if p.is_file()]
if not matches:
return None
return max(matches, key=lambda p: p.stat().st_mtime)
def _glob_log_paths(pm2_name: str) -> tuple[Path, Path]:
logs_dir = pm2_logs_dir()
return logs_dir / f"{pm2_name}-out.log", logs_dir / f"{pm2_name}-error.log"
out_path = Path()
err_path = Path()
if not logs_dir.is_dir():
return out_path, err_path
slugs = {pm2_name, pm2_name.replace("_", "-")}
for slug in slugs:
if not out_path.is_file():
hit = _newest_match(f"{slug}-out*.log", logs_dir)
if hit is not None:
out_path = hit
if not err_path.is_file():
hit = _newest_match(f"{slug}-error*.log", logs_dir)
if hit is not None:
err_path = hit
return out_path, err_path
def resolve_log_paths(pm2_name: str) -> tuple[Path, Path]:
global _path_cache_at
now = time.time()
if now - _path_cache_at > _PATH_CACHE_TTL_SEC:
_path_cache.clear()
_path_cache_at = now
cached = _path_cache.get(pm2_name)
if cached is not None:
return cached
out_path = Path()
err_path = Path()
for proc in _pm2_jlist():
if proc.get("name") != pm2_name:
continue
env = proc.get("pm2_env") or {}
raw_out = (env.get("pm_out_log_path") or "").strip()
raw_err = (env.get("pm_err_log_path") or "").strip()
if raw_out:
out_path = Path(raw_out)
if raw_err:
err_path = Path(raw_err)
break
glob_out, glob_err = _glob_log_paths(pm2_name)
if not out_path.is_file() and glob_out.is_file():
out_path = glob_out
if not err_path.is_file() and glob_err.is_file():
err_path = glob_err
result = (out_path, err_path)
_path_cache[pm2_name] = result
return result
def log_file_paths(pm2_name: str) -> tuple[Path, Path]:
return resolve_log_paths(pm2_name)
def tail_lines(
@@ -72,7 +155,7 @@ def load_system_logs(target: str, lines: int = DEFAULT_LINES) -> dict[str, Any]:
raise KeyError(key)
cfg = LOG_TARGETS[key]
line_count = max(20, min(MAX_LINES, int(lines or DEFAULT_LINES)))
out_path, err_path = log_file_paths(cfg["pm2_name"])
out_path, err_path = resolve_log_paths(cfg["pm2_name"])
return {
"ok": True,
"key": key,
@@ -83,7 +166,7 @@ def load_system_logs(target: str, lines: int = DEFAULT_LINES) -> dict[str, Any]:
"err": tail_lines(err_path, line_count),
"out_exists": out_path.is_file(),
"err_exists": err_path.is_file(),
"out_path": str(out_path),
"err_path": str(err_path),
"out_path": str(out_path) if str(out_path) else "",
"err_path": str(err_path) if str(err_path) else "",
"updated_at": int(time.time()),
}