All checks were successful
CI / backend-tests (pull_request) Successful in 17m59s
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 12s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m18s
CI Trade-In / backend-tests (pull_request) Successful in 5m59s
Владелец попросил вывести продукт в Графану — до этого там были только
технические панели (запросы/латентность/память). Список счётчиков взят из
реально пишущихся событий, а не выдуман:
Мера (tradein-mvp/backend/app/observability/metrics.py):
- mera_estimates_total{outcome=ok|insufficient_data} — POST /estimate,
зеркалит user_events.event_type=estimate_request (294 строки в БД),
insufficient_data — не ошибка, а исход без аналогов.
- mera_address_suggestions_total{found=yes|no} — GET /geocode/suggest,
своего user_events-события у ручки не было.
- mera_reports_exported_total (без лейблов) — GET /estimate/{id}/pdf.
- mera_leads_total (без лейблов) — POST /trade-in/lead.
- mera_support_messages_total{channel=web|anon} — POST /support/messages
и /support/anon/messages, счётчик после успешной доставки в Telegram.
- mera_logins_total{result=success|failed} — рядом с user_events
login_success/login_failed в auth.py (97/453 строк в БД).
Птица (backend/app/observability/metrics.py):
- sitefinder_reports_exported_total{format} — GET .../forecast/export
(md/json/tg/docx/pptx/pdf) и POST .../best-layouts/pdf.
Метки везде — фиксированный литерал из места вызова (outcome/found/channel/
result/format), никогда username/адрес/estimate_id/кадастровый номер —
это ровно то, что взрывает кардинальность ряда у Prometheus.
Дашборд ops/metrics/grafana/dashboards/product.json ("Продуктовые метрики",
uid gendesign-product) — воронка Меры (оценки/подсказки/лиды/отчёты/входы/
поддержка) + экспорт форматов Птицы, часовые increase()-панели без
стекирования (на соседней панели оно уже давало ложную тревогу, PR #3474).
Provisioning тот же, что у apps.json — сканирует директорию, отдельного
конфига не нужно.
ops/metrics/alloy/alloy-apps.alloy проверен: у job "apps" нет relabel-
фильтра по __name__ (в отличие от cadvisor) — новые счётчики уходят в
remote_write как есть, правки не потребовалось.
Refs #3471
94 lines
4.5 KiB
Python
94 lines
4.5 KiB
Python
"""Продуктовые счётчики (#3471): числа бизнеса рядом с техническими метриками.
|
||
|
||
Проверяется то же, ради чего вообще заведён `test_metrics.py` — не «метрика
|
||
существует», а что она не может взорвать кардинальность. Источник счётчиков —
|
||
реальные `event_type` из `user_events` (миграция 184) плюс две ручки без
|
||
собственного audit-события (suggest, PDF). Метки везде — фиксированный литерал
|
||
из места вызова (outcome/found/channel/result), никогда значение из запроса
|
||
(username, адрес, estimate_id), поэтому тест фиксирует именно НАБОР меток, а не
|
||
факт роста счётчика на единицу — рост уже проверен паттерном `test_metrics.py`.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from prometheus_client import REGISTRY, generate_latest
|
||
|
||
from app.observability import metrics as m
|
||
|
||
|
||
def _labelnames(counter: object) -> tuple[str, ...]:
|
||
return tuple(counter._labelnames) # type: ignore[attr-defined]
|
||
|
||
|
||
def test_estimates_counter_has_bounded_outcome_label() -> None:
|
||
"""Только исход расчёта, никогда адрес/estimate_id — те дали бы ряд на заявку."""
|
||
assert _labelnames(m.ESTIMATES) == ("outcome",)
|
||
|
||
|
||
def test_address_suggestions_counter_has_bounded_found_label() -> None:
|
||
"""found — да/нет, не сам адрес и не количество результатов (unbounded)."""
|
||
assert _labelnames(m.ADDRESS_SUGGESTIONS) == ("found",)
|
||
|
||
|
||
def test_reports_exported_counter_has_no_labels() -> None:
|
||
"""У «Меры» один формат отчёта (PDF оценки) — без лейбла, дублировать нечего."""
|
||
assert _labelnames(m.REPORTS_EXPORTED) == ()
|
||
|
||
|
||
def test_leads_counter_has_no_labels() -> None:
|
||
assert _labelnames(m.LEADS) == ()
|
||
|
||
|
||
def test_support_messages_counter_has_bounded_channel_label() -> None:
|
||
"""channel — ровно два значения (web/anon), не thread_id и не username."""
|
||
assert _labelnames(m.SUPPORT_MESSAGES) == ("channel",)
|
||
|
||
|
||
def test_logins_counter_has_bounded_result_label() -> None:
|
||
"""result — исход попытки, не username (иначе ряд на каждый аккаунт)."""
|
||
assert _labelnames(m.LOGINS) == ("result",)
|
||
|
||
|
||
def test_product_counters_survive_a_realistic_sequence() -> None:
|
||
"""Инкременты по реальным меткам видны в экспозиции и не мешают друг другу.
|
||
|
||
Значения сравниваются приращением: реестр `prometheus_client` глобален на
|
||
процесс, и абсолютное число зависит от порядка запуска тестов.
|
||
"""
|
||
|
||
def _value(name: str, labels: dict[str, str]) -> float:
|
||
return REGISTRY.get_sample_value(name, labels) or 0.0
|
||
|
||
before_ok = _value("mera_estimates_total", {"outcome": "ok"})
|
||
before_insufficient = _value("mera_estimates_total", {"outcome": "insufficient_data"})
|
||
before_found = _value("mera_address_suggestions_total", {"found": "yes"})
|
||
before_web = _value("mera_support_messages_total", {"channel": "web"})
|
||
before_login_ok = _value("mera_logins_total", {"result": "success"})
|
||
|
||
m.ESTIMATES.labels(outcome="ok").inc()
|
||
m.ESTIMATES.labels(outcome="insufficient_data").inc()
|
||
m.ADDRESS_SUGGESTIONS.labels(found="yes").inc()
|
||
m.SUPPORT_MESSAGES.labels(channel="web").inc()
|
||
m.LOGINS.labels(result="success").inc()
|
||
m.LEADS.inc()
|
||
m.REPORTS_EXPORTED.inc()
|
||
|
||
assert _value("mera_estimates_total", {"outcome": "ok"}) - before_ok == 1.0
|
||
assert (
|
||
_value("mera_estimates_total", {"outcome": "insufficient_data"}) - before_insufficient
|
||
== 1.0
|
||
)
|
||
assert _value("mera_address_suggestions_total", {"found": "yes"}) - before_found == 1.0
|
||
assert _value("mera_support_messages_total", {"channel": "web"}) - before_web == 1.0
|
||
assert _value("mera_logins_total", {"result": "success"}) - before_login_ok == 1.0
|
||
|
||
body = generate_latest(REGISTRY).decode()
|
||
for metric in (
|
||
"mera_estimates_total",
|
||
"mera_address_suggestions_total",
|
||
"mera_reports_exported_total",
|
||
"mera_leads_total",
|
||
"mera_support_messages_total",
|
||
"mera_logins_total",
|
||
):
|
||
assert metric in body
|