"""读取日志文件末尾 N 行(v1.1.18)。 按 level 过滤:默认 INFO+。适用于 RotatingFileHandler 落地的日志文件。 """ from __future__ import annotations import os from pathlib import Path from util.runtime_paths import get_logs_dir DEFAULT_TAIL = 200 _LEVEL_ORDER = { "DEBUG": 10, "INFO": 20, "WARNING": 30, "ERROR": 40, "CRITICAL": 50, } def _resolve_log_path() -> Path: """解析日志文件路径。 优先级: 1. LOG_FILE env(绝对路径直接用,相对路径走 get_logs_dir()) 2. 默认 get_logs_dir() / LOG_FILE_DEFAULT_NAME """ raw = os.environ.get("LOG_FILE", "") if raw: p = Path(raw) if not p.is_absolute(): p = get_logs_dir() / raw return p from util.constants import LOG_FILE_DEFAULT_NAME return get_logs_dir() / LOG_FILE_DEFAULT_NAME def tail_log_file(tail: int = DEFAULT_TAIL, min_level: str = "INFO") -> dict: """读日志文件末尾 tail 行,过滤 level >= min_level。 Returns: { "log_file": str, "exists": bool, "total_lines": int, "returned_lines": int, "lines": [str, ...], "min_level": str, } """ log_path = _resolve_log_path() if not log_path.exists(): return { "log_file": str(log_path), "exists": False, "total_lines": 0, "returned_lines": 0, "lines": [], "min_level": min_level, } text = log_path.read_text(encoding="utf-8", errors="replace") all_lines = text.splitlines() last_n = all_lines[-tail:] if tail > 0 else all_lines threshold = _LEVEL_ORDER.get(min_level.upper(), 20) filtered: list[str] = [] for line in last_n: # 格式: "2026-09-15 12:34:56 [alert-contract-expiring-hr] INFO message" # 取第 4 个 token (INFO/DEBUG/...) parts = line.split() if len(parts) >= 4 and parts[1].count(":") >= 2 and parts[3] in _LEVEL_ORDER: line_level = parts[3] if _LEVEL_ORDER.get(line_level, 0) >= threshold: filtered.append(line) else: # 非标准行 / 解析不到 level:保留 filtered.append(line) return { "log_file": str(log_path), "exists": True, "total_lines": len(all_lines), "returned_lines": len(filtered), "lines": filtered, "min_level": min_level, }