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