food-market/docs/observability.md
nns ed140cb819
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
docs(s28): api-reference 195→240 + observability + integration #7 + CI
Overnight progress while 4h-soak runs in background:

1. ApiReferenceDocsJob.cs + scripts/gen-api-reference.py — return-type
   regex теперь ловит nested generics любой глубины. Было 195
   endpoint'ов в auto-gen reference; стало 240 (+45). EmployeesController
   GET /api/organization/employees был пропущен из-за
   Task<ActionResult<PagedResult<EmployeeDto>>>.

2. docs/observability.md — добавлен food_market_disk_free_bytes (Sprint 20)
   + раздел "quality-watchdog метрики" (5 метрик textfile exporter'a из
   Sprint 26: run_total, step_failure_total, endpoint_p95_ms,
   last_run_status, incidents_total). Готовые dashboards теперь содержат
   оба JSON (food-market.json + quality-watchdog.json).

3. tests/integration/07-import-export-flows.spec.ts — POST 1C-CSV import
   (semicolon-CSV cp1251) → создаются продукты с группой автоматом;
   POST /api/org/export (НЕ /api/admin/org-export) → возвращает
   {id, status}; orgB не видит export orgA. Прогон 8.2s.

4. tests/food-market.IntegrationTests/PruneQualityTestOrgsTests.cs —
   2 [Fact]'a для метода из Sprint 25. Удаляет только quality-* старше
   threshold, не трогает реальные org. Требует Testcontainers.

5. .forgejo/workflows/regression.yml — добавлен шаг integration suite
   после flows+visual. Telegram: "35 flows + 60 visual + 8 integration".

Soak-real (4h @ 50 RPS) запущен в setsid-detach session, продолжается.
Итоговые числа добавлю в sprint28-progress.md после завершения.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 03:26:39 +05:00

9.3 KiB
Raw Blame History

Observability (Prometheus / Grafana)

food-market.api экспортирует метрики Prometheus на /metrics (text exposition format, без авторизации). На prod закрываем nginx-уровнем (allow private network, deny all) или basic-auth.

Базовые метрики (от prometheus-net)

Метрика Тип Лейблы Что показывает
http_requests_received_total counter code, method, controller, action Сколько HTTP-запросов прошло — split per controller+action+status.
http_request_duration_seconds histogram code, method, controller, action Длительность HTTP, гистограмма для p50/p95/p99 SLO.
process_cpu_seconds_total counter CPU time.
process_resident_memory_bytes gauge RSS.
dotnet_total_memory_bytes gauge Managed heap.
dotnet_collection_count_total counter generation GC count по поколениям.

Кастомные метрики

Метрика Тип Лейблы Семантика
food_market_documents_posted_total counter type Проведено документов (retail-sale, supply, enter, loss, transfer, inventory, supplier-return, customer-return).
food_market_sales_posted_total counter Alias для documents_posted{type="retail-sale"} (явно перечислен в SLO).
food_market_supplies_posted_total counter Alias для documents_posted{type="supply"}.
food_market_documents_error_total counter type, reason Ошибки проведения: reason serialization (40001), insufficient_stock, number_conflict, validation, other.
food_market_db_query_duration_seconds histogram kind Длительность SQL-запросов EF Core. kind=query (SELECT), kind=command (INSERT/UPDATE/DELETE/SCALAR).
food_market_disk_free_bytes gauge mount Sprint 20: свободное место на диске (обновляется ежечасным DiskMonitoringJob).

Tenant-меток в кастомных метриках НЕТ сознательно: на multi-tenant хосте они бы раздули cardinality. Per-org разрез — через /api/reports/* (там authz-фильтр уже работает).

quality-watchdog метрики (Sprint 26+)

~/quality-watchdog.sh после каждого прогона пишет ~/.fm-watchdog/textfile/quality_watchdog.prom — формат Prometheus textfile. Подбирается через node_exporter --collector.textfile.directory=$HOME/.fm-watchdog/textfile.

Метрика Тип Лейблы Семантика
quality_watchdog_run_total counter result Кол-во прогонов watchdog'a, разделённых на green/red.
quality_watchdog_step_failure_total counter step Падений per-step (health, auth_me, products, ui_flow, metrics, signalr, multi_tenant, perf).
quality_watchdog_endpoint_p95_ms gauge endpoint p95 latency последнего прогона per-endpoint.
quality_watchdog_last_run_status gauge 1 если все шаги зелёные, 0 иначе.
quality_watchdog_incidents_total counter Создано incident-файлов (2× consecutive fail) за всё время.

Эти метрики питают deploy/grafana/dashboards/quality-watchdog.json (Sprint 26, 10 панелей).

Scrape-конфиг (prometheus.yml)

scrape_configs:
  - job_name: food-market-api
    metrics_path: /metrics
    scrape_interval: 15s
    static_configs:
      - targets: ['food-market-api:8080']

Готовые Grafana dashboards

В репо два готовых JSON-дашборда:

Файл UID Назначение
deploy/grafana/dashboards/food-market.json fm-baseline Sprint 13 baseline: HTTP / EF / бизнес-метрики
deploy/grafana/dashboards/quality-watchdog.json fm-quality-watchdog Sprint 26: smoke success / p95 / multi-tenant violations / incidents

food-market.json — 9 панелей:

  1. HTTP — RPS по статус-коду (stacked).
  2. HTTP — latency p50/p95/p99 (5-минутный rolling).
  3. Бизнес — документы посчитаны (Post), per-type RPS.
  4. Бизнес — ошибки проведения per-type/reason.
  5. DB — длительность EF-запросов (heatmap).
  6. HTTP — % 5xx за 5 мин (stat-панель с порогами).
  7. HTTP — % 4xx за 5 мин.
  8. Процесс — память (RSS + managed heap).
  9. GC — сборки в секунду по поколениям.

Импорт в Grafana

Через UI:

  1. Grafana → Dashboards → New → Import.
  2. Upload JSON file → выбрать deploy/grafana/dashboards/food-market.json.
  3. Datasource — выбрать Prometheus (по дефолту в шаблонной переменной ${DS_PROMETHEUS} написано «Prometheus»).
  4. Import.

Через CLI (curl к Grafana API, требует Bearer-токен от service-account c ролью Editor):

GRAFANA_URL=http://grafana.local:3000
GRAFANA_TOKEN=<your-sa-token>
DS_UID=$(curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" \
    "$GRAFANA_URL/api/datasources/name/Prometheus" | jq -r .uid)
jq --arg uid "$DS_UID" '
    .dashboard = .;
    .dashboard.id = null;
    .overwrite = true;
    .inputs = [{"name":"DS_PROMETHEUS","type":"datasource","pluginId":"prometheus","value":$uid}];
    {dashboard: .dashboard, overwrite: true, inputs: .inputs, folderId: 0}
' deploy/grafana/dashboards/food-market.json \
| curl -s -X POST -H "Authorization: Bearer $GRAFANA_TOKEN" \
    -H "Content-Type: application/json" \
    -d @- "$GRAFANA_URL/api/dashboards/import"

Через provisioning (когда Grafana поднимается рядом):

# /etc/grafana/provisioning/dashboards/food-market.yaml
apiVersion: 1
providers:
  - name: food-market
    orgId: 1
    folder: 'food-market'
    type: file
    disableDeletion: false
    updateIntervalSeconds: 60
    options:
      path: /etc/grafana/dashboards/food-market

Положить food-market.json в /etc/grafana/dashboards/food-market/.

Альтернатива — минимальный набор панелей (если делать руками):

Health row

  • Request ratesum(rate(http_requests_received_total[5m])) by (code) → стек по 2xx/3xx/4xx/5xx.
  • Error rate (5xx)sum(rate(http_requests_received_total{code=~"5.."}[5m])) с alert > 0.1 req/s (5 минут) → Telegram.
  • p95 latencyhistogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)).

Business row

  • Sales/hourrate(food_market_sales_posted_total[1h]) * 3600.
  • Supplies postedincrease(food_market_supplies_posted_total[1d]).
  • Document errorssum(rate(food_market_documents_error_total[5m])) by (type, reason). Alert serialization rate > 1 req/min: указывает на лок-контеншн Postgres.

Database row

  • EF query ratesum(rate(food_market_db_query_duration_seconds_count[5m])) by (kind).
  • EF query p95histogram_quantile(0.95, sum(rate(food_market_db_query_duration_seconds_bucket[5m])) by (le, kind)).

Runtime row

  • CPUrate(process_cpu_seconds_total[1m]) * 100.
  • Memoryprocess_resident_memory_bytes / 1024 / 1024.
  • GC Gen2 collectionsrate(dotnet_collection_count_total{generation="2"}[5m]).

Alerts (prometheus rules) — пример

groups:
  - name: food-market
    rules:
      - alert: HighErrorRate
        expr: sum(rate(http_requests_received_total{code=~"5.."}[5m])) > 0.1
        for: 5m
        labels: { severity: warning }
        annotations:
          summary: "food-market.api возвращает >0.1 5xx/s"
      - alert: DbSerializationContention
        expr: rate(food_market_documents_error_total{reason="serialization"}[5m]) > 0.016
        for: 10m
        labels: { severity: warning }
        annotations:
          summary: "Сериализационные конфликты EF >1/мин"
      - alert: NoSalesIn30Min
        expr: increase(food_market_sales_posted_total[30m]) == 0
        for: 30m
        labels: { severity: info }
        annotations:
          summary: "Нет продаж 30 минут — POS оффлайн или магазин закрыт"

Локальная отладка

# Чтобы посмотреть метрики из локального API:
curl http://localhost:5081/metrics | head -50

# Конкретная метрика:
curl -s http://localhost:5081/metrics | grep food_market_sales_posted_total

Поведение в тестовом окружении

В интеграционных тестах prometheus-метрики поднимаются как часть WebApplicationFactory; счётчики живут per-process (статические Metrics.Create...). Состояние accumulated между тестами в той же сборке — поэтому в MetricsEndpointTests мы проверяем «значение увеличилось», а не точное число.