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