feat(monitoring): 监控页三分区仪表 + 上游探针(信息丰度增强)

此前监控页 6 张卡只有 2 个布尔有效(版本/运行时间/检查项三张死卡,
后端 /health 从未返回对应字段),与顶栏健康点重复。

后端:
- /health 补 version(importlib.metadata,失败降级 None)与
  uptime_seconds(monotonic,不受时钟跳变影响) —— 激活两张死卡
- 新增 GET /admin/monitoring/upstream:bzzoiro 可达性探针,
  轻量 GET 根端点,不携带 Key、不消耗采集配额;HTTP<500 视为可达
  (DNS/网络/TLS 层面),业务失败由死信与日志上报

前端: Monitoring 重排为三分区仪表
- 基础设施: 存活 / DB 就绪 / 版本+运行时间 / 上游探针(延迟)
- 数据管线: 最近采集成功率 / 死信待处理 / 数据缺口 / 统计覆盖
- LLM: 预测总数 / 平均延迟 / 有效率(<80% 警示色)
- 顶部「需要关注」聚合条: 任一异常亮红并直达处理页,全清显示状态行
- 各信号独立容错(Promise.allSettled),单项失败只降级对应卡片

测试: 新增 test_monitoring_endpoints.py 5 例(ASGITransport 真路由 +
mock 探针外呼);全量 331 passed 8 skipped
This commit is contained in:
WorkBuddy
2026-09-22 20:44:16 +08:00
parent f00bf71e8f
commit 5001093c3b
5 changed files with 414 additions and 106 deletions
+21 -1
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import logging
import os
import time
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
@@ -13,6 +14,9 @@ from src.core.config import settings
logger = logging.getLogger(__name__)
# 进程启动时间(monotonic,不受系统时钟跳变影响):/health 的 uptime 来源
_PROCESS_STARTED_MONOTONIC = time.monotonic()
async def _fail_stale_ingest_jobs() -> None:
"""P1-E: 启动时将上次遗留的 pending/running ingest_jobs 标 failed。
@@ -156,6 +160,7 @@ def create_app() -> FastAPI:
from src.api.routes.auth import router as auth_router
from src.api.routes.admin_settings import router as admin_settings_router
from src.api.routes.schedules import router as schedules_router
from src.api.routes.admin_monitoring import router as admin_monitoring_router
app.include_router(matches_router)
app.include_router(predict_router)
@@ -165,10 +170,25 @@ def create_app() -> FastAPI:
app.include_router(auth_router)
app.include_router(admin_settings_router)
app.include_router(schedules_router)
app.include_router(admin_monitoring_router)
@app.get("/health")
async def health():
return {"status": "healthy", "service": "profeto"}
"""存活检查。version/uptime_seconds 供管理端监控页展示。"""
version = None
try:
from importlib.metadata import version as _pkg_version
version = _pkg_version("profeto")
except Exception:
# 包元数据缺失时返回 None,前端降级隐藏版本卡片
version = None
return {
"status": "healthy",
"service": "profeto",
"version": version,
"uptime_seconds": round(time.monotonic() - _PROCESS_STARTED_MONOTONIC),
}
@app.get("/health/ready")