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
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>
199 lines
9.3 KiB
Markdown
199 lines
9.3 KiB
Markdown
# 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` мы проверяем «значение увеличилось», а не точное число.
|