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:
@@ -1,15 +1,17 @@
|
|||||||
"""Метрики Prometheus: латентность HTTP + gauge'и пайплайна и очередей.
|
"""Метрики Prometheus: латентность HTTP + gauge'и пайплайна, очередей и железа.
|
||||||
|
|
||||||
`GET /metrics` — без авторизации (снаружи закрывается на уровне nginx, вне
|
`GET /metrics` — без авторизации (снаружи закрывается на уровне nginx, вне
|
||||||
периметра backend, см. `docs/deploy/scaling.md`/monitoring-часть devops):
|
периметра backend, см. `docs/deploy/scaling.md`/monitoring-часть devops):
|
||||||
Prometheus-серверы традиционно ходят напрямую в контейнер по внутренней
|
Prometheus-серверы традиционно ходят напрямую в контейнер по внутренней
|
||||||
сети, а не через публичный `/api/`-гейтвей.
|
сети, а не через публичный `/api/`-гейтвей.
|
||||||
|
|
||||||
Gauge'и `vidconf_pipeline_sessions`/`vidconf_celery_queue_depth` намеренно
|
Gauge'и `vidconf_pipeline_sessions`/`vidconf_celery_queue_depth`/
|
||||||
НЕ обновляются фоновой задачей — значения пересчитываются прямо в обработчике
|
`vidconf_host_info` намеренно НЕ обновляются фоновой задачей — значения
|
||||||
запроса при каждом scrape (см. докстринг `metrics_endpoint`), поэтому их
|
пересчитываются прямо в обработчике запроса при каждом scrape (см.
|
||||||
асинхронные источники (БД, Redis) можно опросить обычным `await` вместо
|
докстринг `metrics_endpoint`), поэтому их асинхронные источники (БД,
|
||||||
реализации синхронного `prometheus_client.registry.Collector`.
|
Redis) можно опросить обычным `await` вместо реализации синхронного
|
||||||
|
`prometheus_client.registry.Collector` (у `vidconf_host_info` источник
|
||||||
|
и вовсе синхронный — настройки уже в памяти процесса).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
@@ -20,6 +22,7 @@ from prometheus_client import CONTENT_TYPE_LATEST, Gauge, Histogram, generate_la
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from starlette.routing import Match
|
from starlette.routing import Match
|
||||||
|
|
||||||
|
from core.config import get_settings
|
||||||
from core.db import get_session
|
from core.db import get_session
|
||||||
from core.redis import redis_client
|
from core.redis import redis_client
|
||||||
from models.session import PIPELINE_STATUSES
|
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)
|
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")
|
@router.get("/metrics")
|
||||||
async def metrics_endpoint(session: AsyncSession = Depends(get_session)) -> Response:
|
async def metrics_endpoint(session: AsyncSession = Depends(get_session)) -> Response:
|
||||||
"""Отдать метрики Prometheus в формате text exposition.
|
"""Отдать метрики 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_pipeline_sessions_gauge(session)
|
||||||
await _refresh_celery_queue_depth_gauge()
|
await _refresh_celery_queue_depth_gauge()
|
||||||
|
_refresh_host_info_gauge()
|
||||||
return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)
|
return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from prometheus_client.samples import Sample
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from api import metrics as metrics_module
|
from api import metrics as metrics_module
|
||||||
|
from core.config import get_settings
|
||||||
from core.redis import redis_client
|
from core.redis import redis_client
|
||||||
from models.conference import Conference
|
from models.conference import Conference
|
||||||
from models.session import ConferenceSession
|
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_http_request_duration_seconds" in families
|
||||||
assert "vidconf_pipeline_sessions" in families
|
assert "vidconf_pipeline_sessions" in families
|
||||||
assert "vidconf_celery_queue_depth" 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:
|
async def test_metrics_records_http_latency_by_route(client: httpx.AsyncClient) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user