feat(backend): метрика vidconf_host_info для плашки характеристик сервера

Info-метрика (значение всегда 1, лейблы cpus/ram_mb/gpu_name/vram_mb) —
источник GPU для новой панели «Характеристики сервера» в дашборде «Хост
и контейнеры» (живые CPU/RAM/диск там же берутся из node-exporter,
GPU node-exporter не знает). Данные — уже читаемые Settings.hw_* из .env,
которые install.sh пишет по ADR-004. «—» вместо None/пустой строки —
однозначный прочерк на панели вместо пустого текста.
This commit is contained in:
2026-07-28 01:39:58 +03:00
parent ace5bf7a0d
commit afd4daee65
2 changed files with 72 additions and 6 deletions

View File

@@ -1,15 +1,17 @@
"""Метрики Prometheus: латентность HTTP + gauge'и пайплайна и очередей.
"""Метрики Prometheus: латентность HTTP + gauge'и пайплайна, очередей и железа.
`GET /metrics` — без авторизации (снаружи закрывается на уровне nginx, вне
периметра backend, см. `docs/deploy/scaling.md`/monitoring-часть devops):
Prometheus-серверы традиционно ходят напрямую в контейнер по внутренней
сети, а не через публичный `/api/`-гейтвей.
Gauge'и `vidconf_pipeline_sessions`/`vidconf_celery_queue_depth` намеренно
НЕ обновляются фоновой задачей — значения пересчитываются прямо в обработчике
запроса при каждом scrape (см. докстринг `metrics_endpoint`), поэтому их
асинхронные источники (БД, Redis) можно опросить обычным `await` вместо
реализации синхронного `prometheus_client.registry.Collector`.
Gauge'и `vidconf_pipeline_sessions`/`vidconf_celery_queue_depth`/
`vidconf_host_info` намеренно НЕ обновляются фоновой задачей — значения
пересчитываются прямо в обработчике запроса при каждом scrape (см.
докстринг `metrics_endpoint`), поэтому их асинхронные источники (БД,
Redis) можно опросить обычным `await` вместо реализации синхронного
`prometheus_client.registry.Collector` (у `vidconf_host_info` источник
и вовсе синхронный — настройки уже в памяти процесса).
"""
import time
@@ -20,6 +22,7 @@ from prometheus_client import CONTENT_TYPE_LATEST, Gauge, Histogram, generate_la
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.routing import Match
from core.config import get_settings
from core.db import get_session
from core.redis import redis_client
from models.session import PIPELINE_STATUSES
@@ -110,6 +113,42 @@ async def _refresh_celery_queue_depth_gauge() -> None:
CELERY_QUEUE_DEPTH.labels(queue=queue).set(depth)
# --- Info-метрика обнаруженного железа (install.sh, ADR-004) ---------------
HOST_INFO = Gauge(
"vidconf_host_info",
"Обнаруженное установщиком железо (info-метрика, значение всегда 1, данные в лейблах)",
labelnames=("cpus", "ram_mb", "gpu_name", "vram_mb"),
)
def _hw_label(value: int | None) -> str:
"""`None` (install.sh не запускался либо GPU не обнаружен) → «—», не пустая строка.
Пустой лейбл Grafana отрисовала бы как пустой текст на панели —
прочерк однозначно читается как «не определено».
"""
return str(value) if value is not None else ""
def _refresh_host_info_gauge() -> None:
"""Пересчитать `vidconf_host_info` по текущим `HW_*` настройкам (`core/config.py`).
Источник — `install.sh`, который пишет `HW_CPUS`/`HW_RAM_MB`/
`HW_GPU_NAME`/`HW_VRAM_MB` в `.env` при установке (ADR-004). Живые
CPU/RAM/диск хоста в дашборде «Хост и контейнеры» берутся из
node-exporter напрямую — здесь только то, чего node-exporter не
знает (GPU), плюс дублирование CPU/RAM install.sh для сверки.
"""
settings = get_settings()
HOST_INFO.labels(
cpus=_hw_label(settings.hw_cpus),
ram_mb=_hw_label(settings.hw_ram_mb),
gpu_name=settings.hw_gpu_name or "",
vram_mb=_hw_label(settings.hw_vram_mb),
).set(1)
@router.get("/metrics")
async def metrics_endpoint(session: AsyncSession = Depends(get_session)) -> Response:
"""Отдать метрики Prometheus в формате text exposition.
@@ -122,4 +161,5 @@ async def metrics_endpoint(session: AsyncSession = Depends(get_session)) -> Resp
"""
await _refresh_pipeline_sessions_gauge(session)
await _refresh_celery_queue_depth_gauge()
_refresh_host_info_gauge()
return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)

View File

@@ -20,6 +20,7 @@ from prometheus_client.samples import Sample
from sqlalchemy.ext.asyncio import AsyncSession
from api import metrics as metrics_module
from core.config import get_settings
from core.redis import redis_client
from models.conference import Conference
from models.session import ConferenceSession
@@ -57,6 +58,31 @@ async def test_metrics_endpoint_returns_prometheus_exposition_format(
assert "vidconf_http_request_duration_seconds" in families
assert "vidconf_pipeline_sessions" in families
assert "vidconf_celery_queue_depth" in families
assert "vidconf_host_info" in families
async def test_metrics_host_info_gauge_reflects_settings(
client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
) -> None:
"""`vidconf_host_info` отдаёт `HW_*` из настроек как лейблы, значение всегда 1."""
settings = get_settings()
monkeypatch.setattr(settings, "hw_cpus", 4)
monkeypatch.setattr(settings, "hw_ram_mb", 7937)
monkeypatch.setattr(settings, "hw_gpu_name", None)
monkeypatch.setattr(settings, "hw_vram_mb", None)
response = await client.get("/metrics")
samples = _samples(response.text, "vidconf_host_info")
value = _sample_value(
samples,
suffix="vidconf_host_info",
cpus="4",
ram_mb="7937",
gpu_name="",
vram_mb="",
)
assert value == 1
async def test_metrics_records_http_latency_by_route(client: httpx.AsyncClient) -> None: