prometheus-net.AspNetCore@8.2.1 + EF Core DbCommandInterceptor.
Endpoint: GET /metrics (text exposition, без auth — типичная практика;
на prod закроем nginx allow private-network).
Стандартные метрики (через UseHttpMetrics):
- http_requests_received_total (code/method/controller/action)
- http_request_duration_seconds (histogram, p50/p95/p99 SLO)
- process_cpu_seconds_total / dotnet_total_memory_bytes / GC counters
Кастомные бизнес-метрики (AppMetrics):
- food_market_documents_posted_total{type} — все типы документов
- food_market_sales_posted_total — alias по retail-sale (явно в SLO)
- food_market_supplies_posted_total — alias по supply
- food_market_documents_error_total{type, reason} — ошибки проведения
с разбивкой по причине (serialization=40001, insufficient_stock,
number_conflict, validation, other)
- food_market_db_query_duration_seconds{kind} — гистограмма SQL через
DbMetricsInterceptor (kind=query для SELECT, command для CUD)
Tenant-меток в кастомных метриках НЕТ сознательно: на multi-tenant хосте
раздуло бы cardinality. Per-org разрез — через /api/reports/*.
Counters добавлены в:
- SuppliesController.Post (success + serialization-error)
- RetailSalesController.Post (success)
- PosController.CreateAndPostSaleAsync (success + number_conflict)
docs/observability.md — scrape-конфиг prometheus.yml, образец Grafana
dashboard (4 ряда: Health/Business/Database/Runtime), prometheus rules
с alert'ами (HighErrorRate, DbSerializationContention, NoSalesIn30Min).
Тесты: 3 интеграционных (endpoint доступен и возвращает text/plain с
встроенными метриками; sales counter инкрементится после Post; db_query
гистограмма накапливается).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
116 lines
5.4 KiB
Markdown
116 lines
5.4 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). |
|
||
|
||
Tenant-меток в кастомных метриках НЕТ сознательно: на multi-tenant хосте они
|
||
бы раздули cardinality. Per-org разрез — через `/api/reports/*` (там
|
||
authz-фильтр уже работает).
|
||
|
||
## 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 dashboard
|
||
|
||
Минимальный набор панелей:
|
||
|
||
### 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` мы проверяем «значение увеличилось», а не точное число.
|