From afd4daee65170029528a6da6f9a9d0c4e9163db4 Mon Sep 17 00:00:00 2001 From: Max Ronzhin Date: Tue, 28 Jul 2026 01:39:58 +0300 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=D0=BC=D0=B5=D1=82=D1=80=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20vidconf=5Fhost=5Finfo=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BB=D0=B0=D1=88=D0=BA=D0=B8=20=D1=85=D0=B0=D1=80=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=B5=D1=80=D0=B8=D1=81=D1=82=D0=B8=D0=BA=20=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/пустой строки — однозначный прочерк на панели вместо пустого текста. --- backend/api/metrics.py | 52 +++++++++++++++++++++++++++---- backend/tests/test_metrics_api.py | 26 ++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/backend/api/metrics.py b/backend/api/metrics.py index a001fa8..9a59349 100644 --- a/backend/api/metrics.py +++ b/backend/api/metrics.py @@ -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) diff --git a/backend/tests/test_metrics_api.py b/backend/tests/test_metrics_api.py index 801e5d2..47eb33a 100644 --- a/backend/tests/test_metrics_api.py +++ b/backend/tests/test_metrics_api.py @@ -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: