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>
5.4 KiB
5.4 KiB
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)
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). Alertserialization 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) — пример
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 оффлайн или магазин закрыт"
Локальная отладка
# Чтобы посмотреть метрики из локального 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 мы проверяем «значение увеличилось», а не точное число.