Sprint 13 — security + observability deep. 7 пунктов чек-листа ✓.
Подробности — docs/sprint13-progress.md и docs/food-market-server-postgres-role.md.
Главное:
- food-market-server (back.food-market.kz, legacy backend) теперь
работает на dedicated PG-роли food_market_server_app (NOSUPERUSER /
NOCREATEDB / NOCREATEROLE / NOREPLICATION / NOBYPASSRLS) с CRUD-only
грантами. Раньше использовался postgres-superuser с паролем 1q2w3e4r.
Бэкап конфига сохранён, rollback одной командой.
- SecurityHeadersMiddleware навешивает CSP / X-Frame-Options DENY /
X-Content-Type-Options nosniff / Referrer-Policy strict-origin /
Permissions-Policy. HSTS 365d + includeSubDomains + preload.
Те же заголовки в deploy/nginx.conf для SPA HTML.
- Rate-limit:
• Signup-IP — 3/час + 10/день (на stage'е переопределено через
.env RATE_SIGNUP_HOUR=30 чтобы не ломать e2e).
• Forgot-password — per-email 3/час + per-IP 10/час.
- SensitiveOpsAudit сервис, wired в:
• TwoFactor enroll/disable
• Employees.Update при смене RoleId (action=AssignRole,
payload с prev/next role + полный RolePermissions)
• MeAccount.ChangePassword (новый endpoint)
• MeSessions.RevokeAll (новый endpoint)
- POST /api/me/sessions/revoke-all — через
IOpenIddictAuthorizationManager.FindBySubjectAsync + TryRevokeAsync.
Integration-тест: refresh после revoke → 400/401.
- Hangfire dashboard — nginx-route добавлен (раньше /hangfire ловился
SPA-fallback'ом). Фильтр SuperAdmin'ом уже был. Тест: anon/tenant →
401/403/404.
- Grafana dashboard JSON (deploy/grafana/dashboards/food-market.json,
9 панелей) + инструкции импорта в docs/observability.md.
Проверено на stage'е: все 6 security-заголовков видны на /;
/hangfire → 401 (закрыт); 4-я форгот → 429; stage-smoke (5 этапов) ✓.
Тесты: 68 unit + 9 integration (включая 3 новых: SessionRevokeTests,
HangfireAccessTests).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
400 lines
12 KiB
JSON
400 lines
12 KiB
JSON
{
|
|
"annotations": {
|
|
"list": [
|
|
{
|
|
"builtIn": 1,
|
|
"datasource": {
|
|
"type": "grafana",
|
|
"uid": "-- Grafana --"
|
|
},
|
|
"enable": true,
|
|
"hide": true,
|
|
"iconColor": "rgba(0, 211, 255, 1)",
|
|
"name": "Annotations & Alerts",
|
|
"type": "dashboard"
|
|
}
|
|
]
|
|
},
|
|
"description": "Sprint 13 baseline-dashboard для food-market.api. Объединяет prometheus-net (HTTP), EF Core (DB) и кастомные AppMetrics (бизнес).",
|
|
"editable": true,
|
|
"fiscalYearStartMonth": 0,
|
|
"graphTooltip": 1,
|
|
"id": null,
|
|
"links": [],
|
|
"liveNow": false,
|
|
"panels": [
|
|
{
|
|
"datasource": {
|
|
"type": "prometheus",
|
|
"uid": "${DS_PROMETHEUS}"
|
|
},
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"color": {"mode": "palette-classic"},
|
|
"custom": {
|
|
"axisCenteredZero": false,
|
|
"axisColorMode": "text",
|
|
"axisLabel": "",
|
|
"axisPlacement": "auto",
|
|
"barAlignment": 0,
|
|
"drawStyle": "line",
|
|
"fillOpacity": 10,
|
|
"gradientMode": "none",
|
|
"hideFrom": {"legend": false, "tooltip": false, "viz": false},
|
|
"lineInterpolation": "linear",
|
|
"lineWidth": 2,
|
|
"pointSize": 5,
|
|
"scaleDistribution": {"type": "linear"},
|
|
"showPoints": "never",
|
|
"spanNulls": false,
|
|
"stacking": {"group": "A", "mode": "none"},
|
|
"thresholdsStyle": {"mode": "off"}
|
|
},
|
|
"mappings": [],
|
|
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}]},
|
|
"unit": "reqps"
|
|
},
|
|
"overrides": []
|
|
},
|
|
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
|
|
"id": 1,
|
|
"options": {
|
|
"legend": {"calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true},
|
|
"tooltip": {"mode": "multi", "sort": "desc"}
|
|
},
|
|
"targets": [
|
|
{
|
|
"expr": "sum by (code) (rate(http_requests_received_total[1m]))",
|
|
"legendFormat": "{{code}}",
|
|
"refId": "A"
|
|
}
|
|
],
|
|
"title": "HTTP — RPS по статус-коду",
|
|
"type": "timeseries"
|
|
},
|
|
{
|
|
"datasource": {
|
|
"type": "prometheus",
|
|
"uid": "${DS_PROMETHEUS}"
|
|
},
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"color": {"mode": "thresholds"},
|
|
"mappings": [],
|
|
"thresholds": {
|
|
"mode": "absolute",
|
|
"steps": [
|
|
{"color": "green", "value": null},
|
|
{"color": "yellow", "value": 0.5},
|
|
{"color": "red", "value": 2}
|
|
]
|
|
},
|
|
"unit": "s"
|
|
}
|
|
},
|
|
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
|
|
"id": 2,
|
|
"options": {
|
|
"legend": {"calcs": ["last", "max"], "displayMode": "table", "placement": "right", "showLegend": true},
|
|
"tooltip": {"mode": "multi", "sort": "desc"}
|
|
},
|
|
"targets": [
|
|
{
|
|
"expr": "histogram_quantile(0.50, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))",
|
|
"legendFormat": "p50",
|
|
"refId": "A"
|
|
},
|
|
{
|
|
"expr": "histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))",
|
|
"legendFormat": "p95",
|
|
"refId": "B"
|
|
},
|
|
{
|
|
"expr": "histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))",
|
|
"legendFormat": "p99",
|
|
"refId": "C"
|
|
}
|
|
],
|
|
"title": "HTTP — latency p50/p95/p99",
|
|
"type": "timeseries"
|
|
},
|
|
{
|
|
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"color": {"mode": "palette-classic"},
|
|
"custom": {
|
|
"drawStyle": "line",
|
|
"fillOpacity": 10,
|
|
"lineWidth": 2,
|
|
"pointSize": 5,
|
|
"showPoints": "never",
|
|
"spanNulls": false,
|
|
"stacking": {"group": "A", "mode": "normal"}
|
|
},
|
|
"mappings": [],
|
|
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}]},
|
|
"unit": "ops"
|
|
}
|
|
},
|
|
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 8},
|
|
"id": 3,
|
|
"options": {
|
|
"legend": {"calcs": ["sum"], "displayMode": "table", "placement": "right", "showLegend": true},
|
|
"tooltip": {"mode": "multi", "sort": "desc"}
|
|
},
|
|
"targets": [
|
|
{
|
|
"expr": "sum by (type) (rate(food_market_documents_posted_total[1m]))",
|
|
"legendFormat": "{{type}}",
|
|
"refId": "A"
|
|
}
|
|
],
|
|
"title": "Бизнес — документы посчитаны (Post), per-type RPS",
|
|
"type": "timeseries"
|
|
},
|
|
{
|
|
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"color": {"mode": "thresholds"},
|
|
"mappings": [],
|
|
"thresholds": {
|
|
"mode": "absolute",
|
|
"steps": [
|
|
{"color": "green", "value": null},
|
|
{"color": "yellow", "value": 0.1},
|
|
{"color": "red", "value": 1}
|
|
]
|
|
},
|
|
"unit": "ops"
|
|
}
|
|
},
|
|
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 8},
|
|
"id": 4,
|
|
"options": {
|
|
"legend": {"calcs": ["sum"], "displayMode": "table", "placement": "right", "showLegend": true},
|
|
"tooltip": {"mode": "multi", "sort": "desc"}
|
|
},
|
|
"targets": [
|
|
{
|
|
"expr": "sum by (type, reason) (rate(food_market_documents_error_total[1m]))",
|
|
"legendFormat": "{{type}} / {{reason}}",
|
|
"refId": "A"
|
|
}
|
|
],
|
|
"title": "Бизнес — ошибки проведения per-type / reason",
|
|
"type": "timeseries"
|
|
},
|
|
{
|
|
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"color": {"mode": "spectrum", "scheme": "Blues"},
|
|
"custom": {
|
|
"hideFrom": {"legend": false, "tooltip": false, "viz": false},
|
|
"scaleDistribution": {"type": "linear"}
|
|
},
|
|
"mappings": [],
|
|
"unit": "s"
|
|
}
|
|
},
|
|
"gridPos": {"h": 9, "w": 12, "x": 0, "y": 16},
|
|
"id": 5,
|
|
"options": {
|
|
"calculate": false,
|
|
"cellGap": 1,
|
|
"color": {"exponent": 0.5, "fill": "dark-orange", "mode": "scheme", "reverse": false, "scale": "exponential", "scheme": "Spectral", "steps": 64},
|
|
"exemplars": {"color": "rgba(255,0,255,0.7)"},
|
|
"filterValues": {"le": 1e-9},
|
|
"legend": {"show": true},
|
|
"rowsFrame": {"layout": "auto"},
|
|
"tooltip": {"show": true, "yHistogram": false},
|
|
"yAxis": {"axisPlacement": "left", "reverse": false, "unit": "s"}
|
|
},
|
|
"pluginVersion": "10.0.0",
|
|
"targets": [
|
|
{
|
|
"expr": "sum by (le) (rate(food_market_db_query_duration_seconds_bucket[1m]))",
|
|
"format": "heatmap",
|
|
"legendFormat": "{{le}}",
|
|
"refId": "A"
|
|
}
|
|
],
|
|
"title": "DB — длительность EF-запросов (heatmap)",
|
|
"type": "heatmap"
|
|
},
|
|
{
|
|
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"color": {"mode": "thresholds"},
|
|
"thresholds": {
|
|
"mode": "absolute",
|
|
"steps": [
|
|
{"color": "green", "value": null},
|
|
{"color": "yellow", "value": 5},
|
|
{"color": "red", "value": 20}
|
|
]
|
|
},
|
|
"unit": "percent"
|
|
}
|
|
},
|
|
"gridPos": {"h": 9, "w": 6, "x": 12, "y": 16},
|
|
"id": 6,
|
|
"options": {
|
|
"colorMode": "background",
|
|
"graphMode": "area",
|
|
"justifyMode": "center",
|
|
"orientation": "auto",
|
|
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
|
|
"textMode": "auto"
|
|
},
|
|
"pluginVersion": "10.0.0",
|
|
"targets": [
|
|
{
|
|
"expr": "100 * sum(rate(http_requests_received_total{code=~\"5..\"}[5m])) / sum(rate(http_requests_received_total[5m]))",
|
|
"refId": "A"
|
|
}
|
|
],
|
|
"title": "HTTP — % 5xx за 5 мин",
|
|
"type": "stat"
|
|
},
|
|
{
|
|
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"color": {"mode": "thresholds"},
|
|
"thresholds": {
|
|
"mode": "absolute",
|
|
"steps": [
|
|
{"color": "green", "value": null},
|
|
{"color": "yellow", "value": 10},
|
|
{"color": "red", "value": 30}
|
|
]
|
|
},
|
|
"unit": "percent"
|
|
}
|
|
},
|
|
"gridPos": {"h": 9, "w": 6, "x": 18, "y": 16},
|
|
"id": 7,
|
|
"options": {
|
|
"colorMode": "background",
|
|
"graphMode": "area",
|
|
"justifyMode": "center",
|
|
"orientation": "auto",
|
|
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
|
|
"textMode": "auto"
|
|
},
|
|
"pluginVersion": "10.0.0",
|
|
"targets": [
|
|
{
|
|
"expr": "100 * sum(rate(http_requests_received_total{code=~\"4..\"}[5m])) / sum(rate(http_requests_received_total[5m]))",
|
|
"refId": "A"
|
|
}
|
|
],
|
|
"title": "HTTP — % 4xx за 5 мин",
|
|
"type": "stat"
|
|
},
|
|
{
|
|
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"color": {"mode": "palette-classic"},
|
|
"custom": {
|
|
"drawStyle": "line",
|
|
"fillOpacity": 10,
|
|
"lineWidth": 2,
|
|
"pointSize": 5,
|
|
"showPoints": "never"
|
|
},
|
|
"mappings": [],
|
|
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}]},
|
|
"unit": "bytes"
|
|
}
|
|
},
|
|
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 25},
|
|
"id": 8,
|
|
"options": {
|
|
"legend": {"calcs": ["last", "max"], "displayMode": "table", "placement": "right", "showLegend": true},
|
|
"tooltip": {"mode": "multi", "sort": "desc"}
|
|
},
|
|
"targets": [
|
|
{
|
|
"expr": "process_resident_memory_bytes",
|
|
"legendFormat": "RSS",
|
|
"refId": "A"
|
|
},
|
|
{
|
|
"expr": "dotnet_total_memory_bytes",
|
|
"legendFormat": "Managed heap",
|
|
"refId": "B"
|
|
}
|
|
],
|
|
"title": "Процесс — память (RSS + managed)",
|
|
"type": "timeseries"
|
|
},
|
|
{
|
|
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"color": {"mode": "palette-classic"},
|
|
"custom": {
|
|
"drawStyle": "line",
|
|
"fillOpacity": 10,
|
|
"lineWidth": 2,
|
|
"pointSize": 5,
|
|
"showPoints": "never"
|
|
},
|
|
"mappings": [],
|
|
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}]},
|
|
"unit": "ops"
|
|
}
|
|
},
|
|
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 25},
|
|
"id": 9,
|
|
"options": {
|
|
"legend": {"calcs": ["sum"], "displayMode": "table", "placement": "right", "showLegend": true},
|
|
"tooltip": {"mode": "multi", "sort": "desc"}
|
|
},
|
|
"targets": [
|
|
{
|
|
"expr": "rate(dotnet_collection_count_total[1m])",
|
|
"legendFormat": "Gen {{generation}}",
|
|
"refId": "A"
|
|
}
|
|
],
|
|
"title": "GC — сборки в секунду (по поколениям)",
|
|
"type": "timeseries"
|
|
}
|
|
],
|
|
"refresh": "30s",
|
|
"schemaVersion": 39,
|
|
"tags": ["food-market", "api"],
|
|
"templating": {
|
|
"list": [
|
|
{
|
|
"current": {"selected": false, "text": "Prometheus", "value": "Prometheus"},
|
|
"hide": 0,
|
|
"includeAll": false,
|
|
"label": "Datasource",
|
|
"multi": false,
|
|
"name": "DS_PROMETHEUS",
|
|
"options": [],
|
|
"query": "prometheus",
|
|
"refresh": 1,
|
|
"regex": "",
|
|
"skipUrlSync": false,
|
|
"type": "datasource"
|
|
}
|
|
]
|
|
},
|
|
"time": {"from": "now-1h", "to": "now"},
|
|
"timepicker": {},
|
|
"timezone": "browser",
|
|
"title": "food-market — api / db / business",
|
|
"uid": "food-market-api-baseline",
|
|
"version": 1,
|
|
"weekStart": ""
|
|
}
|