Deal — единая кодовая база
ci / build-test (push) Canceled after 0s

SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/
Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue,
контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер),
Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог).

Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
This commit is contained in:
Rustam Khalimov
2026-09-11 23:56:47 +03:00
commit 27c7831910
1383 changed files with 158436 additions and 0 deletions
@@ -0,0 +1,388 @@
{
"annotations": {
"list": []
},
"editable": true,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Успешные входы (HTTP 200) по access-логу core (HttpAccessLogMiddleware: поля Path/StatusCode). Тенант — POST /api/auth/login, оператор — POST /api/operator/auth/login.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum(count_over_time({service=\"core\"} | json | Path=\"/api/auth/login\" | StatusCode=200 [1m]))",
"legendFormat": "тенант",
"refId": "A"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum(count_over_time({service=\"core\"} | json | Path=\"/api/operator/auth/login\" | StatusCode=200 [1m]))",
"legendFormat": "оператор",
"refId": "B"
}
],
"title": "Успешные входы, /мин",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Неудачные входы по HTTP-коду ответа: 401 — неверные учётные данные, 403 — тенант приостановлен, 429 — сработал LoginAttemptGuard (5 неудач ip|login за 15 мин). По access-логу core: поля Path/StatusCode.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "bars",
"fillOpacity": 60,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum by (StatusCode) (count_over_time({service=\"core\"} | json | Path=~\"/api/auth/login|/api/operator/auth/login\" | StatusCode >= 400 [1m]))",
"legendFormat": "HTTP {{StatusCode}}",
"refId": "A"
}
],
"title": "Неудачные входы по коду ответа, /мин",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Выходы из системы (POST /api/auth/logout — тенант, POST /api/operator/auth/logout — оператор) по access-логу core. Ручка всегда отвечает 200.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum by (Path) (count_over_time({service=\"core\"} | json | Path=~\"/api/auth/logout|/api/operator/auth/logout\" [1m]))",
"legendFormat": "{{Path}}",
"refId": "A"
}
],
"title": "Выходы (logout), /мин",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Активация приглашений (POST /api/join) по коду ответа: 200 — успешная активация, 4xx — неверный/просроченный код, занятый email или приостановленный тенант.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "bars",
"fillOpacity": 60,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum by (StatusCode) (count_over_time({service=\"core\"} | json | Path=\"/api/join\" [1m]))",
"legendFormat": "HTTP {{StatusCode}}",
"refId": "A"
}
],
"title": "Активация инвайтов (/api/join), /мин",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Успешные входы тенанта и оператора за последний час.",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 6,
"x": 0,
"y": 16
},
"id": 5,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum(count_over_time({service=\"core\"} | json | Path=~\"/api/auth/login|/api/operator/auth/login\" | StatusCode=200 [1h]))",
"legendFormat": "успешных входов",
"refId": "A"
}
],
"title": "Успешные входы за 1ч",
"type": "stat"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Неудачные входы тенанта и оператора за последний час; >0 — повод посмотреть Explore (текст ответа) и аудит-ленту оператора.",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 1
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 6,
"x": 6,
"y": 16
},
"id": 6,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum(count_over_time({service=\"core\"} | json | Path=~\"/api/auth/login|/api/operator/auth/login\" | StatusCode >= 400 [1h]))",
"legendFormat": "неудачных входов",
"refId": "A"
}
],
"title": "Неудачные входы за 1ч",
"type": "stat"
},
{
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 16
},
"id": 7,
"options": {
"content": "**Кто именно входил — в аудите, не в логах.** Access-лог core не пишет идентичность (login/tenant/operator) — только метод/путь/код, поэтому панели выше различают лишь контур (тенант vs оператор) по префиксу пути.\n\nПолная лента входов/выходов/неудач с актором, тенантом и IP — в PostgreSQL (`public.audit_log`, append-only, события `tenant_login_ok`/`tenant_login_failed`/`operator_login_*`/`*_logout`/`invite_activated`) через операторские ручки `GET /api/operator/audit` или экран «Аудит» в оператор-консоли.",
"mode": "markdown"
},
"title": "Где искать актора",
"type": "text"
}
],
"refresh": "1m",
"schemaVersion": 39,
"tags": [
"deal"
],
"templating": {
"list": []
},
"time": {
"from": "now-24h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Deal-Auth",
"uid": "deal-auth",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,410 @@
{
"annotations": {
"list": []
},
"editable": true,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "HTTP-ответы 5xx по путям (access-лог core, поле StatusCode). Ручка, отдающая 5xx, обычно соответствует исключению ниже (в той же панели логов).",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "bars",
"fillOpacity": 60,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum by (Path) (count_over_time({service=\"core\"} | json | StatusCode >= 500 [1m]))",
"legendFormat": "{{Path}}",
"refId": "A"
}
],
"title": "HTTP 5xx по путям (core), /мин",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Строки с необработанным исключением (Serilog-поле \"@x\") по deal-процессам. Каждое такое событие пишется access-логом/аудитом и означает баг либо недоступную зависимость.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "bars",
"fillOpacity": 60,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum by (service) (count_over_time({service=~\"core|telegram-service|ai-service|ml-service\"} |~ \"\\\"@x\\\":\\\"\" [1m]))",
"legendFormat": "{{service}}",
"refId": "A"
}
],
"title": "Исключения по сервисам, /мин",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Строки уровня Error/Fatal (Serilog-поле @l) по deal-процессам. Дополняет панель исключений: ловит и логированные, но не брошенные ошибки.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 20,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum by (service) (count_over_time({service=~\"core|telegram-service|ai-service|ml-service\"} |~ \"@l\\\":\\\"(Error|Fatal)\" [1m]))",
"legendFormat": "{{service}}",
"refId": "A"
}
],
"title": "Error/Fatal по сервисам, /мин",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "gRPC-вызовы с не-OK статусом (access-лог RpcCallLoggingInterceptor: поля RpcMethod/GrpcStatus). Cancelled/DeadlineExceeded — клиентские отмены, Unavailable/Unknown — сбои. gRPC-health не логируется.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "bars",
"fillOpacity": 60,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum by (service) (count_over_time({service=~\"core|telegram-service|ai-service|ml-service\"} |= \"gRPC \" | json | GrpcStatus != \"OK\" [1m]))",
"legendFormat": "{{service}}",
"refId": "A"
}
],
"title": "gRPC-вызовы с ошибкой по сервисам, /мин",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Суммарно Error/Fatal по всем deal-процессам за 5 минут; >0 — повод открыть панель логов ниже.",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 1
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 0,
"y": 16
},
"id": 5,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum(count_over_time({service=~\"core|telegram-service|ai-service|ml-service\"} |~ \"@l\\\":\\\"(Error|Fatal)\" [5m]))",
"legendFormat": "ошибок за 5м",
"refId": "A"
}
],
"title": "Error/Fatal за 5 мин (все сервисы)",
"type": "stat"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "HTTP 5xx (core) за последний час.",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 1
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 16
},
"id": 6,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum(count_over_time({service=\"core\"} | json | StatusCode >= 500 [1h]))",
"legendFormat": "HTTP 5xx за 1ч",
"refId": "A"
}
],
"title": "HTTP 5xx за 1 час (core)",
"type": "stat"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Строки Error/Fatal и записи с исключением (\"@x\") по всем deal-процессам. Разверните строку для текста исключения и стека.",
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 21
},
"id": 7,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": true
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "{service=~\"core|telegram-service|ai-service|ml-service\"} |~ \"@l\\\":\\\"(Error|Fatal)\"",
"refId": "A"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "{service=~\"core|telegram-service|ai-service|ml-service\"} |~ \"\\\"@x\\\":\\\"\"",
"refId": "B"
}
],
"title": "Лента ошибок и исключений",
"type": "logs"
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": [
"deal"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Deal-Errors",
"uid": "deal-errors",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,228 @@
{
"annotations": {
"list": []
},
"editable": true,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Строк логов в минуту по контейнерам deal-процессов (core/telegram-service/ai-service/ml-service). Контейнер без логов = нет данных (сервис молчит или лежит).",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 15,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "count_over_time({service=\"core\"}[1m])",
"legendFormat": "core",
"refId": "A"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "count_over_time({service=\"telegram-service\"}[1m])",
"legendFormat": "telegram-service",
"refId": "B"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "count_over_time({service=\"ai-service\"}[1m])",
"legendFormat": "ai-service",
"refId": "C"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "count_over_time({service=\"ml-service\"}[1m])",
"legendFormat": "ml-service",
"refId": "D"
}
],
"title": "Активность логов deal-процессов (строк/мин)",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Строки уровня Error/Fatal (Serilog CompactJsonFormatter: \"@l\":\"Error\"/\"Fatal\") по deal-процессам. Поиск по логам детально — Explore с датасорсом Loki.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "bars",
"fillOpacity": 60,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "count_over_time({service=~\"core|telegram-service|ai-service|ml-service\"} |~ \"@l\\\":\\\"(Error|Fatal)\" [1m])",
"legendFormat": "{{service}}",
"refId": "A"
}
],
"title": "Ошибки и фатальные по deal-процессам (строк/мин)",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Суммарно Error/Fatal по всем deal-процессам за 5 минут; >0 — повод открыть Explore и посмотреть текст ошибки.",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 1
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 24,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum(count_over_time({service=~\"core|telegram-service|ai-service|ml-service\"} |~ \"@l\\\":\\\"(Error|Fatal)\" [5m]))",
"legendFormat": "ошибок за 5м",
"refId": "A"
}
],
"title": "Ошибки за 5 минут (все deal-процессы)",
"type": "stat"
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": [
"deal"
],
"templating": {
"list": []
},
"time": {
"from": "now-15m",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Deal-Health",
"uid": "deal-health",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,303 @@
{
"annotations": {
"list": []
},
"editable": true,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Живая лента логов deal-процессов: фильтры по сервису и уровню сверху. Разверните строку — детали Serilog-события (@mt, свойства, исключение). Query-строка/тела запросов в логи не пишутся (Ruling 13).",
"gridPos": {
"h": 12,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": true
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "{service=~\"$service\", level=~\"$level\"}",
"refId": "A"
}
],
"title": "Логи deal-процессов",
"type": "logs"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Объём логов в разрезе уровня Serilog (поле @l, поднято в метку level пайплайном Promtail). Высокий Error/Fatal — детали в дашборде Deal-Errors.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 12
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum by (level) (count_over_time({service=~\"$service\", level=~\"$level\"} [1m]))",
"legendFormat": "{{level}}",
"refId": "A"
}
],
"title": "Строк логов по уровням, /мин",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Объём логов по сервисам (метка service из лейблов Promtail). Заметный провал до нуля — сервис молчит или лежит (см. также Deal-Health).",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 12
},
"id": 3,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum by (service) (count_over_time({service=~\"$service\"} [1m]))",
"legendFormat": "{{service}}",
"refId": "A"
}
],
"title": "Строк логов по сервисам, /мин",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "События с идентификатором тенанта (Serilog-поле TenantId) в логах фоновых сервисов: AI/ML-вызовы («Аудит: tenant …»), Telegram (backfill/realtime). Показывает, какие тенанты активны, по данным логов.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 20
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "topk(10, sum by (TenantId) (count_over_time({service=~\"ai-service|ml-service|telegram-service\"} | json | TenantId =~ \".+\" [5m])))",
"legendFormat": "{{TenantId}}",
"refId": "A"
}
],
"title": "Активность по тенантам (AI/ML/Telegram, top-10), /5мин",
"type": "timeseries"
},
{
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 20
},
"id": 5,
"options": {
"content": "**Действия пользователей логируются не сюда, а в аудит.** Создание/перемещение/удаление карточек, контейнеры, настройки, каналы, входы/выходы/инвайты пишутся append-only в `public.audit_log` (AuditService) и доступны через `GET /api/operator/audit` / экран «Аудит» оператор-консоли.\n\nВ логах (этат дашборд) по действиям доступны: access-логи HTTP/gRPC (метод/путь/код), аудит-строки фоновых сервисов (`Аудит: tenant …`) и идентификатор тенанта (`TenantId`). Поиск по ключевой фразе добавьте в ленту: `{service=~\"$service\"} |= \"фраза\"`.",
"mode": "markdown"
},
"title": "Где искать действия пользователей",
"type": "text"
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": [
"deal"
],
"templating": {
"list": [
{
"allValue": ".*",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "loki",
"uid": "loki"
},
"definition": "label_values(service)",
"includeAll": true,
"label": "Сервис",
"multi": true,
"name": "service",
"options": [],
"query": "label_values(service)",
"refresh": 2,
"regex": "",
"sort": 1,
"type": "query"
},
{
"allValue": ".*",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"definition": "Information,Warning,Error,Fatal",
"includeAll": true,
"label": "Уровень",
"multi": true,
"name": "level",
"options": [
{
"selected": false,
"text": "Information",
"value": "Information"
},
{
"selected": false,
"text": "Warning",
"value": "Warning"
},
{
"selected": false,
"text": "Error",
"value": "Error"
},
{
"selected": false,
"text": "Fatal",
"value": "Fatal"
}
],
"query": "Information,Warning,Error,Fatal",
"type": "custom"
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Deal-Logs",
"uid": "deal-logs",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,606 @@
{
"annotations": {
"list": []
},
"editable": true,
"graphTooltip": 1,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "Суммарная нагрузка всех deal-процессов (HTTP + gRPC) в секунду. Источник — OTel-инструментация ASP.NET Core (метрика http.server.request.duration, счётчик _count), scrape /metrics :9464.",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 6,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(rate(http_server_request_duration_seconds_count[$__rate_interval]))",
"legendFormat": "RPS",
"refId": "A"
}
],
"title": "RPS (HTTP+gRPC) сейчас",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "Активные непросроченные сессии пользователей тенантов и операторов (public.sessions + public.operator_sessions). Агрегат по всем тенантам — в метке нет tenantId (низкая кардинальность).",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 6,
"x": 6,
"y": 0
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "deal_sessions_active",
"legendFormat": "сессии",
"refId": "A"
}
],
"title": "Активные сессии",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "Суммарная глубина очереди пайплайна (new+filtered) по всем тенантам; считает фоновый DealMetricsCollector через PipelineProcessingService.QueueCountsAsync.",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 6,
"x": 12,
"y": 0
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "deal_pipeline_queue_depth",
"legendFormat": "очередь",
"refId": "A"
}
],
"title": "Очередь пайплайна",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "Суммарная глубина очереди обучения ML (MlOutbox) по всем тенантам; считает фоновый DealMetricsCollector через IMlLearningStore.CountOutboxAsync. Рост при недоступности ml-service.",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 6,
"x": 18,
"y": 0
},
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "deal_ml_outbox_depth",
"legendFormat": "outbox",
"refId": "A"
}
],
"title": "Очередь ML-outbox",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "RPS по сервисам (метка service из таргетов Prometheus). Включает HTTP-запросы core и gRPC-вызовы всех 4 процессов (gRPC идёт как ASP.NET Core-запрос с route = имя метода).",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 5
},
"id": 5,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (service) (rate(http_server_request_duration_seconds_count[$__rate_interval]))",
"legendFormat": "{{service}}",
"refId": "A"
}
],
"title": "RPS по сервисам",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "p95 длительности запроса по сервисам (OTel-гистограмма http.server.request.duration). Рост при недоступности БД/внешних вызовов (ИИ-провайдер, ml/telegram-service).",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 5
},
"id": 6,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "histogram_quantile(0.95, sum by (le, service) (rate(http_server_request_duration_seconds_bucket[$__rate_interval])))",
"legendFormat": "{{service}}",
"refId": "A"
}
],
"title": "Длительность запроса p95 по сервисам",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "Доля HTTP/gRPC-запросов с кодом ответа 5xx в секунду по сервисам (метка http_response_status_code).",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 13
},
"id": 7,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (service) (rate(http_server_request_duration_seconds_count{http_response_status_code=~\"5..\"}[$__rate_interval]))",
"legendFormat": "{{service}}",
"refId": "A"
}
],
"title": "Ошибки 5xx по сервисам",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "Токены платного ИИ (по видам prompt/completion) и оценка токенов локальных ML-вызовов, токенов/сек. Инкремент — общая с ядром точка записи token_usage_events (TokenUsageRecorder).",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 13
},
"id": 8,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (type) (rate(deal_ai_tokens_total[$__rate_interval]))",
"legendFormat": "ai/{{type}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(rate(deal_ml_tokens_total[$__rate_interval]))",
"legendFormat": "ml/prompt",
"refId": "B"
}
],
"title": "Токены AI/ML, tokens/s",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "Вызовы платного ИИ и локального ML в секунду (соответствуют событиям token_usage_events: kind=ai|ml).",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 21
},
"id": 9,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(rate(deal_ai_calls_total[$__rate_interval]))",
"legendFormat": "AI-вызовы",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum(rate(deal_ml_calls_total[$__rate_interval]))",
"legendFormat": "ML-вызовы",
"refId": "B"
}
],
"title": "Вызовы AI/ML, calls/s",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "Топ-10 типов событий аудита по частоте (метки event/actor низкокардинальные; tenantId/userId в метрики не попадают). Полная лента с актором — public.audit_log через GET /api/operator/audit.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 21
},
"id": 10,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "topk(10, sum by (event) (rate(deal_audit_events_total[$__rate_interval])))",
"legendFormat": "{{event}}",
"refId": "A"
}
],
"title": "События аудита по типам (top-10)",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": [
"deal",
"metrics"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Deal-Metrics-Overview",
"uid": "deal-metrics",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,375 @@
{
"annotations": {
"list": []
},
"editable": true,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "HTTP-запросы core в секунду (access-лог HttpAccessLogMiddleware; строка «HTTP <метод> <путь> -> <код>»). gRPC-ингресс здесь не виден — его считают панели gRPC (HTTP-статус gRPC-вызовов всегда 200).",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum(rate({service=\"core\"} |= \"HTTP \" [1m]))",
"legendFormat": "HTTP core",
"refId": "A"
}
],
"title": "HTTP RPS (core)",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "RPC-вызовы gRPC в секунду по deal-процессам (access-лог RpcCallLoggingInterceptor; строка «gRPC <метод>: <статус>»). gRPC-health не логируется — фоновые liveness-пробы в счёт не попадают.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum by (service) (rate({service=~\"core|telegram-service|ai-service|ml-service\"} |= \"gRPC \" [1m]))",
"legendFormat": "{{service}}",
"refId": "A"
}
],
"title": "gRPC RPS по сервисам",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Топ-10 HTTP-путей core по частоте вызовов, запросов/сек. По access-логу core (поле Path, query-строка не логируется).",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 3,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "topk(10, sum by (Path) (rate({service=\"core\"} | json | Path =~ \".+\" [5m])))",
"legendFormat": "{{Path}}",
"refId": "A"
}
],
"title": "HTTP RPS по путям (top-10, core)",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Перцентили длительности запроса core (access-лог core, поле DurationMs — миллисекунды всего пути). p95 резко растёт при недоступности БД/внешних вызовов.",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "ms"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "quantile_over_time(0.5, {service=\"core\"} | json | DurationMs =~ \"[0-9]+\" | unwrap DurationMs [1m])",
"legendFormat": "p50",
"refId": "A"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "quantile_over_time(0.95, {service=\"core\"} | json | DurationMs =~ \"[0-9]+\" | unwrap DurationMs [1m])",
"legendFormat": "p95",
"refId": "B"
}
],
"title": "Длительность HTTP-запросов core (p50/p95), мс",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Топ-10 gRPC-методов по частоте вызовов (access-лог gRPC, поле RpcMethod — полное имя метода с пакетом).",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"showPoints": "never"
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 16
},
"id": 5,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "topk(10, sum by (RpcMethod) (rate({service=~\"core|telegram-service|ai-service|ml-service\"} |= \"gRPC \" | json | RpcMethod =~ \".+\" [5m])))",
"legendFormat": "{{RpcMethod}}",
"refId": "A"
}
],
"title": "gRPC-методы по частоте (top-10)",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"description": "Суммарная нагрузка (HTTP + gRPC) в секунду по всем deal-процессам за последнюю минуту.",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 0,
"y": 24
},
"id": 6,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum(rate({service=~\"core|telegram-service|ai-service|ml-service\"} |~ \"(HTTP|gRPC) \" [1m]))",
"legendFormat": "RPS",
"refId": "A"
}
],
"title": "RPS (HTTP+gRPC) сейчас",
"type": "stat"
},
{
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 24
},
"id": 7,
"options": {
"content": "RPS/нагрузка получены из access-логов (Promtail → Loki) как ленточное дополнение к метрикам. С этапа 12 (пакет A) RPS/латентность/ошибки есть и в OTel-метриках → Prometheus: дашборд Deal-Metrics-Overview (источник — Prometheus). Строка на запрос: HTTP-путь и gRPC-метод пишутся HTTP access-логом и RpcCallLoggingInterceptor без query/заголовков/тел — секретов в этих панелях нет.",
"mode": "markdown"
},
"title": "Откуда эти цифры",
"type": "text"
}
],
"refresh": "1m",
"schemaVersion": 39,
"tags": [
"deal"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Deal-Rps",
"uid": "deal-rps",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,16 @@
# Grafana — provisioning папки дашбордов (Ruling 7/9, план Task 14).
# Дашборды из каталога /var/lib/grafana/dashboards (монтируется из deploy/observability/grafana/dashboards)
# появляются в Grafana автоматически; правка — файлами в репозитории, не через UI.
apiVersion: 1
providers:
- name: deal
orgId: 1
folder: Дейл
type: file
disableDeletion: true
updateIntervalSeconds: 30
allowUiUpdates: false
options:
path: /var/lib/grafana/dashboards
@@ -0,0 +1,28 @@
# Grafana — provisioning датасорсов Loki (логи) и Prometheus (метрики) — Ruling 7/9; метрики — этап 12, пакет A.
# Монтируется в /etc/grafana/provisioning/datasources (compose.prod.yml).
apiVersion: 1
datasources:
- name: Loki
# UID фиксирован: на него ссылаются панели дашбордов Deal-Health/Auth/Errors/Rps/Logs (datasource uid: loki).
uid: loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
jsonData:
maxLines: 1000
- name: Prometheus
# UID фиксирован: на него ссылаются панели дашборда Deal-Metrics-Overview (datasource uid: prometheus).
# URL — сервис compose-профиля observability; метрики scrape'ит сам Prometheus с /metrics процессов.
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: false
jsonData:
# Prometheus хранит OTel-гистограммы в нативных bucket'ах — используем нативные histogram_quantile.
httpMethod: POST
timeInterval: 15s
+45
View File
@@ -0,0 +1,45 @@
# Loki — хранилище логов deal-стека (Ruling 7/9, план Task 14). Single-binary режим, файловое
# хранилище (без object-store): данные — /loki на volume deal_loki_data.
#
# Retention — 7 суток (168h): включается compactor'ом (retention_enabled) + limits_config.
# Это сознательно «логи на неделю»: долгосрочное хранение/выгрузка — rolling-файлы Serilog
# data/logs/deal-*.json (у core — на volume /app/data, см. compose.prod.yml).
auth_enabled: false
server:
http_listen_port: 3100
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
# Период хранения: 7 суток (168 часов). События старше удаляются compactor'ом (см. ниже).
limits_config:
retention_period: 168h
compactor:
working_directory: /loki/compactor
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
# Loki 3.x: при retention_enabled требуется store для delete-запросов (файловое хранилище).
delete_request_store: filesystem
+81
View File
@@ -0,0 +1,81 @@
# Правила алертов Prometheus для процессов Deal (этап 12, пакет A; Ruling 7 этапа 7).
#
# Подключаются в deploy/observability/prometheus.yml через `rule_files`. Источник метрик — эндпоинты
# /metrics процессов (OTel → экспортёр Prometheus), метка target `service` (core/telegram-service/
# ai-service/ml-service). Имена метрик — в legacy-схеме (global.metric_name_validation_scheme: legacy),
# поэтому точки OTel-имён экранированы в `_` (`deal.sessions.active` → `deal_sessions_active`).
#
# Пороги подобраны консервативно и осознанно «мягкие»: это задел оператору, а не жёсткий SLO —
# уточняйте под реальный профиль нагрузки. Токен-бюджет в Prometheus НЕ алертится намеренно: метрики
# бюджета (`deal_*`) нет (метки низкокардинальные, без tenantId/бюджета). Исчерпание ИИ-бюджета
# доставляется тенанту SSE-тостом (BudgetAlertScheduler), см. техдок §6/§7/§10.
groups:
- name: deal-availability
rules:
# Сервис не отвечает на scrape (недоступен): конфиг включает 4 процесса Deal + сам Prometheus.
- alert: DealServiceDown
expr: up{job="deal"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Сервис Deal недоступен — {{ $labels.service }}"
description: "Prometheus не может снять /metrics с {{ $labels.instance }} (service={{ $labels.service }}) более 2 минут."
# Пропажа метрик ядра: scrape ядра или datasource недоступен, хотя Prometheus жив.
- alert: DealCoreMetricsAbsent
expr: absent(deal_sessions_active)
for: 5m
labels:
severity: critical
annotations:
summary: "Нет метрик ядра Deal"
description: "Метрика deal_sessions_active отсутствует более 5 минут — не скрейпится ядро (core:9464) или datasource недоступен."
- name: deal-errors
rules:
# Рост доли 5xx: gRPC-вызовы тоже попадают в http_server_request_duration_seconds_* (gRPC-ингресс).
- alert: DealHigh5xxRatio
expr: |
sum by (service) (rate(http_server_request_duration_seconds_count{http_response_status_code=~"5.."}[5m]))
/
clamp_min(sum by (service) (rate(http_server_request_duration_seconds_count[5m])), 0.001) > 0.05
for: 10m
labels:
severity: warning
annotations:
summary: "Рост 5xx в сервисе {{ $labels.service }}"
description: "Доля 5xx в {{ $labels.service }} выше 5% за 5 минут (текущее значение {{ $value | humanizePercentage }})."
# Абсолютный всплеск 5xx — ловит рост ошибок и на низком трафике, где доля не показательна.
- alert: Deal5xxBurst
expr: sum by (service) (rate(http_server_request_duration_seconds_count{http_response_status_code=~"5.."}[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "Всплеск 5xx в сервисе {{ $labels.service }}"
description: "Более 1 ответа 5xx/с в {{ $labels.service }} за 5 минут ({{ $value }}/с)."
- name: deal-queues
rules:
# Лаг очереди пайплайна: воркер ходит каждые 2 с, устойчивая глубина = воркер не успевает/залип.
- alert: DealPipelineQueueBacklog
expr: deal_pipeline_queue_depth > 100
for: 15m
labels:
severity: warning
annotations:
summary: "Растёт очередь пайплайна Deal"
description: "Суммарная глубина очереди пайплайна (new+filtered) держится выше 100 более 15 минут ({{ $value }})."
# Лаг ML-outbox: outbox-цикл идёт каждые 10 с; устойчивый рост = ml-service недоступен/отстаёт.
- alert: DealMlOutboxBacklog
expr: deal_ml_outbox_depth > 100
for: 15m
labels:
severity: warning
annotations:
summary: "Растёт очередь обучения ML (outbox)"
description: "Суммарная глубина MlOutbox держится выше 100 более 15 минут ({{ $value }})."
+48
View File
@@ -0,0 +1,48 @@
# Prometheus — сбор метрик Deal-процессов (этап 12, пакет A; Ruling 7 этапа 7).
#
# Источник — эндпоинты /metrics процессов Deal (OpenTelemetry → экспортёр Prometheus). Процессы
# слушают метрики на ОТДЕЛЬНОМ HTTP/1.1 Kestrel-эндпоинте :9464 (gRPC-порты :51015103/:5082 — HTTP/2,
# обычный GET scrape по ним невозможен). Порт метрик наружу не публикуется: scrape идёт внутри
# compose-сети от этого контейнера. Метка service (целевая) используется панелями Grafana
# Deal-Metrics-Overview для разбивки RPS/латентности/ошибок по процессам.
#
# Поднимается профилем observability (deploy/compose.prod.yml); конфиг монтируется в
# /etc/prometheus/prometheus.yml. Retention задаётся ключом --storage.tsdb.retention.time (15 суток).
global:
scrape_interval: 15s
evaluation_interval: 15s
# Имена метрик — в legacy-схеме Prometheus (точки в OTel-именах экранируются в _): без этой опции
# scrape-запрос Prometheus 3 запрашивает UTF-8-схему, и метрики сохраняются с точками
# (`deal.sessions.active`), что ломает привычные PromQL-имена (`deal_sessions_active`).
metric_name_validation_scheme: legacy
# Правила алертов (этап 12) — монтируются рядом с этим конфигом (/etc/prometheus/prometheus-rules.yml).
# Путь относительный — резолвится от каталога конфига (/etc/prometheus). Правила см. в
# deploy/observability/prometheus-rules.yml.
rule_files:
- prometheus-rules.yml
scrape_configs:
# Само-мониторинг Prometheus (доступность самого коллектора).
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
# 4 процесса «Дейла»: core (:5080 HTTP + :5082 gRPC-ингресс) и три gRPC-сервиса.
# У каждого — свой /metrics на :9464; target-метка service попадает во все серии.
- job_name: deal
metrics_path: /metrics
static_configs:
- targets: ["core:9464"]
labels:
service: core
- targets: ["telegram-service:9464"]
labels:
service: telegram-service
- targets: ["ai-service:9464"]
labels:
service: ai-service
- targets: ["ml-service:9464"]
labels:
service: ml-service
+52
View File
@@ -0,0 +1,52 @@
# Promtail — сбор логов контейнеров deal-стека для Loki (Ruling 7/9, план Task 14).
#
# Источник — docker-логи ВСЕХ контейнеров хоста через docker_sd (нужен доступ к docker.sock, см.
# compose.prod.yml). Контейнеры deal-процессов пишут в stdout одну JSON-строку на событие
# (Serilog CompactJsonFormatter, Ruling 7) — лог можно парсить в Loki через «| json» без доп. шаблонов.
# relabel добавляет метки: container (имя контейнера) и service (имя compose-сервиса из стандартной
# метки com.docker.compose.service) — по ним строятся дашборды Grafana (Deal-Health, Deal-Auth,
# Deal-Errors, Deal-Rps, Deal-Logs). Pipeline дополнительно поднимает поле Serilog "@l" в метку
# level (Information/Warning/Error/Fatal) — по ней фильтруют панели Deal-Logs.
server:
http_listen_port: 9080
grpc_listen_port: 0
# Позиции чтения — на volume deal_promtail_data (переживают рестарт контейнера).
positions:
filename: /var/lib/promtail/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
# Имя контейнера (docker присваивает <проект>-<сервис>-<n> или container_name).
- source_labels: ["__meta_docker_container_name"]
regex: "/(.*)"
target_label: container
# Имя compose-сервиса (core/telegram-service/ai-service/ml-service/...) из метки compose.
- source_labels: ["__meta_docker_container_label_com_docker_compose_service"]
target_label: service
# Поток stdout/stderr — отдельной меткой (у Serilog весь вывод — stdout).
- source_labels: ["__meta_docker_container_log_stream"]
target_label: stream
pipeline_stages:
# Пайплайн применяется ТОЛЬКО к deal-процессам (match по метке service из relabel выше):
# их логи — одна JSON-строка Serilog CompactJsonFormatter на событие. Прочие контейнеры
# хоста (postgres/minio/caddy/loki/...) пишут не-JSON — их пайплайн не трогает.
- match:
selector: '{service=~"core|telegram-service|ai-service|ml-service"}'
stages:
# Поле Serilog "@l" (ключ со спецсимволом @ — литерал JMESPath в одинарных кавычках).
- json:
expressions:
level: '"@l"'
# drop_malformed не задан (false) — строка без валидного JSON не роняется, а проходит
# без метки level (деградация мягкая).
- labels:
level: