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

199 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)
```yaml
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):
```bash
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 поднимается рядом):
```yaml
# /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 rate** — `sum(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 latency** — `histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))`.
### Business row
* **Sales/hour** — `rate(food_market_sales_posted_total[1h]) * 3600`.
* **Supplies posted** — `increase(food_market_supplies_posted_total[1d])`.
* **Document errors** — `sum(rate(food_market_documents_error_total[5m])) by (type, reason)`.
Alert `serialization rate > 1 req/min`: указывает на лок-контеншн Postgres.
### Database row
* **EF query rate** — `sum(rate(food_market_db_query_duration_seconds_count[5m])) by (kind)`.
* **EF query p95** — `histogram_quantile(0.95,
sum(rate(food_market_db_query_duration_seconds_bucket[5m])) by (le, kind))`.
### Runtime row
* **CPU** — `rate(process_cpu_seconds_total[1m]) * 100`.
* **Memory** — `process_resident_memory_bytes / 1024 / 1024`.
* **GC Gen2 collections** — `rate(dotnet_collection_count_total{generation="2"}[5m])`.
## Alerts (prometheus rules) — пример
```yaml
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 оффлайн или магазин закрыт"
```
## Локальная отладка
```bash
# Чтобы посмотреть метрики из локального 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` мы проверяем «значение увеличилось», а не точное число.