Нормализовать переводы строк в LF
Решение по TD-STYLE-ANALYZERS: LF — инструменты проекта (Python/Node) пишут LF, CRLF-.sh не работают на Linux CI (sh scripts/ci.sh), большинство файлов уже были LF. Добавлен .gitattributes (* text=auto eol=lf, бинарные исключения), .editorconfig переведён на lf, 1029 файлов конвертированы, git add --renormalize. Из индекса убраны закравшиеся archive/**/__pycache__/*.pyc.
This commit is contained in:
+5
-5
@@ -1,5 +1,5 @@
|
||||
DEAL_PG_HOST=localhost
|
||||
DEAL_PG_PORT=5433
|
||||
DEAL_PG_DB=deal
|
||||
DEAL_PG_USER=deal
|
||||
DEAL_PG_PASSWORD=deal_dev_password
|
||||
DEAL_PG_HOST=localhost
|
||||
DEAL_PG_PORT=5433
|
||||
DEAL_PG_DB=deal
|
||||
DEAL_PG_USER=deal
|
||||
DEAL_PG_PASSWORD=deal_dev_password
|
||||
|
||||
+62
-62
@@ -1,62 +1,62 @@
|
||||
# deploy/.env.prod.example — шаблон production-окружения «Дейла» (compose.prod.yml, Ruling 9; Task 14).
|
||||
#
|
||||
# Использование:
|
||||
# cp deploy/.env.prod.example deploy/.env.prod
|
||||
# # заполнить ВСЕ секреты (пустое значение = config упадёт с подсказкой — fail-fast, Ruling 9)
|
||||
# docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml config # проверка
|
||||
# docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml up -d --build
|
||||
# # + наблюдаемость (профиль): ... up -d --build --profile observability
|
||||
#
|
||||
# Пароли/токены/ключи НЕ имеют значений-дефолтов (генерация: openssl rand -base64 32 и т.п.).
|
||||
# DEAL_OPERATOR_* можно оставить пустыми: оператор тогда НЕ создаётся при старте (Ruling 1) —
|
||||
# заведёте позже env + рестартом core.
|
||||
|
||||
# ── Обязательные секреты (пусто → docker compose config: ошибка с именем переменной) ──
|
||||
|
||||
# Пароль Postgres (пользователь/БД фиксированы: deal/deal).
|
||||
DEAL_PG_PASSWORD=
|
||||
|
||||
# Ключ AES-256-GCM шифрования секретов настроек core (32 байта, base64): openssl rand -base64 32.
|
||||
# Альтернатива — файл data/encryption.key на volume deal_api_data (см. техдок §13.4a).
|
||||
DEAL_ENCRYPTION_KEY=
|
||||
|
||||
# Общий service-token процессов (gRPC-metadata; один на весь стек).
|
||||
DEAL_SERVICE_TOKEN=
|
||||
|
||||
# Ключ AES-256-GCM файлов сессий telegram-service (32 байта, base64): openssl rand -base64 32.
|
||||
DEAL_TELEGRAM_SESSION_KEY=
|
||||
|
||||
# Креды MinIO (root; S3 API и консоль). Бакет deal-files создаётся лениво (Ruling 4).
|
||||
MINIO_ROOT_USER=
|
||||
MINIO_ROOT_PASSWORD=
|
||||
|
||||
# Origin фронта для CORS/Origin-проверки core (Ruling 9/10). При статике за caddy тот же домен:
|
||||
DEAL_ALLOWED_ORIGINS=https://deal.example
|
||||
|
||||
# ── Оператор (Ruling 1): пусто = bootstrap пропущен (стартовый warning), заводится позже env+рестарт ──
|
||||
DEAL_OPERATOR_LOGIN=
|
||||
DEAL_OPERATOR_PASSWORD=
|
||||
|
||||
# ── mTLS внутреннего gRPC (Ruling 6; Task 13/14) ──
|
||||
# 0/пусто — plaintext + service-token внутри изолированной сети; 1 — TLS + клиентский сертификат.
|
||||
# Сертификаты: запустите scripts/mtls-certs.sh (deploy/certs/*) и задайте пароль PFX:
|
||||
DEAL_MTLS_ENABLED=0
|
||||
DEAL_MTLS_CERT_PASSWORD=
|
||||
|
||||
# При DEAL_MTLS_ENABLED=1 схемы внутренних endpoint'ов обязаны стать https:// (иначе рукопожатие
|
||||
# падает — код схему сам не переключает, см. task-13-report). Порт/имя менять не нужно:
|
||||
DEAL_ML_ENDPOINT=http://ml-service:5103
|
||||
DEAL_AI_ENDPOINT=http://ai-service:5102
|
||||
DEAL_TELEGRAM_ENDPOINT=http://telegram-service:5101
|
||||
DEAL_CORE_INGRESS=http://core:5082
|
||||
|
||||
# ── Необязательные (дефолты в compose.prod.yml) ──
|
||||
# Каталог сертификатов mTLS на хосте (монтируется в /etc/deal/certs контейнеров):
|
||||
# DEAL_CERTS_DIR=./certs
|
||||
# Дефолт-бюджет ИИ нового тенанта (токенов/месяц; константа кода — 10 000 000):
|
||||
# DEAL_DEFAULT_AI_BUDGET=
|
||||
|
||||
# ── Observability-профиль (только при --profile observability; см. compose.prod.yml) ──
|
||||
# Пароль admin'а Grafana. Задать ПЕРЕД подъёмом профиля (пусто — grafana не получит пароль):
|
||||
DEAL_GRAFANA_ADMIN_PASSWORD=
|
||||
# DEAL_GRAFANA_ADMIN_USER=admin
|
||||
# deploy/.env.prod.example — шаблон production-окружения «Дейла» (compose.prod.yml, Ruling 9; Task 14).
|
||||
#
|
||||
# Использование:
|
||||
# cp deploy/.env.prod.example deploy/.env.prod
|
||||
# # заполнить ВСЕ секреты (пустое значение = config упадёт с подсказкой — fail-fast, Ruling 9)
|
||||
# docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml config # проверка
|
||||
# docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml up -d --build
|
||||
# # + наблюдаемость (профиль): ... up -d --build --profile observability
|
||||
#
|
||||
# Пароли/токены/ключи НЕ имеют значений-дефолтов (генерация: openssl rand -base64 32 и т.п.).
|
||||
# DEAL_OPERATOR_* можно оставить пустыми: оператор тогда НЕ создаётся при старте (Ruling 1) —
|
||||
# заведёте позже env + рестартом core.
|
||||
|
||||
# ── Обязательные секреты (пусто → docker compose config: ошибка с именем переменной) ──
|
||||
|
||||
# Пароль Postgres (пользователь/БД фиксированы: deal/deal).
|
||||
DEAL_PG_PASSWORD=
|
||||
|
||||
# Ключ AES-256-GCM шифрования секретов настроек core (32 байта, base64): openssl rand -base64 32.
|
||||
# Альтернатива — файл data/encryption.key на volume deal_api_data (см. техдок §13.4a).
|
||||
DEAL_ENCRYPTION_KEY=
|
||||
|
||||
# Общий service-token процессов (gRPC-metadata; один на весь стек).
|
||||
DEAL_SERVICE_TOKEN=
|
||||
|
||||
# Ключ AES-256-GCM файлов сессий telegram-service (32 байта, base64): openssl rand -base64 32.
|
||||
DEAL_TELEGRAM_SESSION_KEY=
|
||||
|
||||
# Креды MinIO (root; S3 API и консоль). Бакет deal-files создаётся лениво (Ruling 4).
|
||||
MINIO_ROOT_USER=
|
||||
MINIO_ROOT_PASSWORD=
|
||||
|
||||
# Origin фронта для CORS/Origin-проверки core (Ruling 9/10). При статике за caddy тот же домен:
|
||||
DEAL_ALLOWED_ORIGINS=https://deal.example
|
||||
|
||||
# ── Оператор (Ruling 1): пусто = bootstrap пропущен (стартовый warning), заводится позже env+рестарт ──
|
||||
DEAL_OPERATOR_LOGIN=
|
||||
DEAL_OPERATOR_PASSWORD=
|
||||
|
||||
# ── mTLS внутреннего gRPC (Ruling 6; Task 13/14) ──
|
||||
# 0/пусто — plaintext + service-token внутри изолированной сети; 1 — TLS + клиентский сертификат.
|
||||
# Сертификаты: запустите scripts/mtls-certs.sh (deploy/certs/*) и задайте пароль PFX:
|
||||
DEAL_MTLS_ENABLED=0
|
||||
DEAL_MTLS_CERT_PASSWORD=
|
||||
|
||||
# При DEAL_MTLS_ENABLED=1 схемы внутренних endpoint'ов обязаны стать https:// (иначе рукопожатие
|
||||
# падает — код схему сам не переключает, см. task-13-report). Порт/имя менять не нужно:
|
||||
DEAL_ML_ENDPOINT=http://ml-service:5103
|
||||
DEAL_AI_ENDPOINT=http://ai-service:5102
|
||||
DEAL_TELEGRAM_ENDPOINT=http://telegram-service:5101
|
||||
DEAL_CORE_INGRESS=http://core:5082
|
||||
|
||||
# ── Необязательные (дефолты в compose.prod.yml) ──
|
||||
# Каталог сертификатов mTLS на хосте (монтируется в /etc/deal/certs контейнеров):
|
||||
# DEAL_CERTS_DIR=./certs
|
||||
# Дефолт-бюджет ИИ нового тенанта (токенов/месяц; константа кода — 10 000 000):
|
||||
# DEAL_DEFAULT_AI_BUDGET=
|
||||
|
||||
# ── Observability-профиль (только при --profile observability; см. compose.prod.yml) ──
|
||||
# Пароль admin'а Grafana. Задать ПЕРЕД подъёмом профиля (пусто — grafana не получит пароль):
|
||||
DEAL_GRAFANA_ADMIN_PASSWORD=
|
||||
# DEAL_GRAFANA_ADMIN_USER=admin
|
||||
|
||||
+48
-48
@@ -1,48 +1,48 @@
|
||||
# Caddyfile «Дейла» — PROD edge (compose.prod.yml, Ruling 9/10(3), план Task 14).
|
||||
#
|
||||
# Единственная точка входа наружу: :80/:443. Статика фронта (src/frontend/dist — собирается отдельно:
|
||||
# cd src/frontend && npm run build
|
||||
# ) + reverse_proxy /api/* → core:5080 (HTTP внутри compose-сети). TLS — «tls internal»: Caddy отдаёт
|
||||
# самоподписанный сертификат своего локального CA. Это ПЛЕЙСХОЛДЕР первичного подъёма:
|
||||
# * реальный домен: замените адрес сайта на https://deal.example.com и УБЕРИТЕ «tls internal» —
|
||||
# Caddy выпустит сертификат Let's Encrypt автоматически (нужны публичные 80/443);
|
||||
# * сайт за Cloudflare: вместо этого — origin-сертификат Cloudflare (см. техдок §10);
|
||||
# * доступ по IP без домена: самоподписанный сертификат неизбежен — наружу лучше ничего не открывать,
|
||||
# работать по SSH-туннелю.
|
||||
#
|
||||
# CSP/HSTS сознательно НЕ включены (Ruling 10(3)): CSP для Vue требует nonce-механику (инлайн-стили
|
||||
# карточек, SSE) и включается точечно под реальный фронт; HSTS — только после реального TLS-сертификата.
|
||||
# Заготовки обеих директив — закомментированы ниже в header-блоках.
|
||||
|
||||
https://deal.example {
|
||||
# Плейсхолдер TLS (см. шапку): реальный домен → удалить эту директиву (Caddy сам выпустит Let's Encrypt).
|
||||
tls internal
|
||||
|
||||
# API core: весь путь /api/* уходит на core:5080 БЕЗ перезаписи (контракт /api неизменен, api-map §3).
|
||||
# SSE (/api/events), файлы и QR-SVG проходят reverse_proxy потоково (буферизации нет).
|
||||
handle /api/* {
|
||||
header {
|
||||
# Security-заголовки ответов API (Ruling 10(3); CSP/HSTS — заготовки, см. шапку).
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "no-referrer"
|
||||
# Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
}
|
||||
reverse_proxy core:5080
|
||||
}
|
||||
|
||||
# Статика Vue (src/frontend/dist смонтирована в /srv). SPA-fallback: неизвестный путь отдаёт
|
||||
# index.html — маршруты фронта обрабатывает сам Vue (история браузера).
|
||||
handle {
|
||||
header {
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "no-referrer"
|
||||
# Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
# Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-{NONCE}'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'"
|
||||
}
|
||||
root * /srv
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
# Caddyfile «Дейла» — PROD edge (compose.prod.yml, Ruling 9/10(3), план Task 14).
|
||||
#
|
||||
# Единственная точка входа наружу: :80/:443. Статика фронта (src/frontend/dist — собирается отдельно:
|
||||
# cd src/frontend && npm run build
|
||||
# ) + reverse_proxy /api/* → core:5080 (HTTP внутри compose-сети). TLS — «tls internal»: Caddy отдаёт
|
||||
# самоподписанный сертификат своего локального CA. Это ПЛЕЙСХОЛДЕР первичного подъёма:
|
||||
# * реальный домен: замените адрес сайта на https://deal.example.com и УБЕРИТЕ «tls internal» —
|
||||
# Caddy выпустит сертификат Let's Encrypt автоматически (нужны публичные 80/443);
|
||||
# * сайт за Cloudflare: вместо этого — origin-сертификат Cloudflare (см. техдок §10);
|
||||
# * доступ по IP без домена: самоподписанный сертификат неизбежен — наружу лучше ничего не открывать,
|
||||
# работать по SSH-туннелю.
|
||||
#
|
||||
# CSP/HSTS сознательно НЕ включены (Ruling 10(3)): CSP для Vue требует nonce-механику (инлайн-стили
|
||||
# карточек, SSE) и включается точечно под реальный фронт; HSTS — только после реального TLS-сертификата.
|
||||
# Заготовки обеих директив — закомментированы ниже в header-блоках.
|
||||
|
||||
https://deal.example {
|
||||
# Плейсхолдер TLS (см. шапку): реальный домен → удалить эту директиву (Caddy сам выпустит Let's Encrypt).
|
||||
tls internal
|
||||
|
||||
# API core: весь путь /api/* уходит на core:5080 БЕЗ перезаписи (контракт /api неизменен, api-map §3).
|
||||
# SSE (/api/events), файлы и QR-SVG проходят reverse_proxy потоково (буферизации нет).
|
||||
handle /api/* {
|
||||
header {
|
||||
# Security-заголовки ответов API (Ruling 10(3); CSP/HSTS — заготовки, см. шапку).
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "no-referrer"
|
||||
# Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
}
|
||||
reverse_proxy core:5080
|
||||
}
|
||||
|
||||
# Статика Vue (src/frontend/dist смонтирована в /srv). SPA-fallback: неизвестный путь отдаёт
|
||||
# index.html — маршруты фронта обрабатывает сам Vue (история браузера).
|
||||
handle {
|
||||
header {
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "no-referrer"
|
||||
# Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
# Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-{NONCE}'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'"
|
||||
}
|
||||
root * /srv
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
|
||||
+270
-270
@@ -1,270 +1,270 @@
|
||||
# compose.dev.yml — dev-стек Дейла (этап 6, Task 20; Ruling 12).
|
||||
#
|
||||
# Полный стек: postgres + minio (хранилища), три автономных сервиса этапа 6
|
||||
# (telegram-service :5101, ai-service :5102, ml-service :5103, storage-service :5104) и core (deal-api:
|
||||
# HTTP :5080 + gRPC-ингресс :5082).
|
||||
#
|
||||
# Режимы интеграций core — флагами SERVICES__{ML,AI,TELEGRAM}__USELOCAL (Ruling 6):
|
||||
# * этот файл — сквозной gRPC-режим ВСЕГО стека «по-настоящему»: UseLocal=false заданы env
|
||||
# core-сервиса ниже, интеграции ходят в ml/ai/telegram-сервисы этой же compose-сети
|
||||
# (MlOutboxFlushScheduler → TrainBatch, GrpcAiClassifier/GrpcAiTools, GrpcTelegramClient).
|
||||
# Подъём всего стека одной командой: docker compose -f deploy/compose.dev.yml up -d --build
|
||||
# (см. также scripts/dev-smoke.sh — сквозная проверка с авто-очисткой).
|
||||
# * Local-заглушки (дефолт КОДА — appsettings.json, UseLocal:true) сохраняются для запуска core
|
||||
# с хоста БЕЗ сервисов (обычная dev-разработка, curl-приёмки этапов 2–5): поднимаются только
|
||||
# postgres/minio, core — dotnet run из src/core/Deal.Api. Код-дефолт не меняется — режим
|
||||
# переключает этот compose-файл (env ниже).
|
||||
#
|
||||
# Секреты dev (переопределяются .env/экспортом, шаблон ${VAR:-default}):
|
||||
# DEAL_SERVICE_TOKEN — единый service-token всех процессов (metadata gRPC, Ruling 1/2/13)
|
||||
# DEAL_ENCRYPTION_KEY — ключ AES-256-GCM шифрования секретов настроек core (32 байта base64)
|
||||
# DEAL_TELEGRAM_SESSION_KEY — ключ AES-256-GCM файлов сессий telegram-service (32 байта base64)
|
||||
# минио/БД creds — dev-значения ниже (в проде — этап 7, compose-prod + mTLS).
|
||||
#
|
||||
# Core в этом файле — «весь стек в docker» (эндпоинты — имена compose-сети). Альтернатива — core
|
||||
# с хоста (Ruling 12): поднимаются только postgres/minio и (по желанию) сервисы, core запускается
|
||||
# dotnet run, и сервисы ходят в ингресс хоста через DEAL_CORE_INGRESS=http://host.docker.internal:5082
|
||||
# (см. env telegram-service ниже).
|
||||
|
||||
# Общий hardening прикладных сервисов (BL-IMG-HARDEN): read-only root FS + tmpfs /tmp,
|
||||
# запрет новых привилегий и сброс capabilities. Данные — только в именованных volume.
|
||||
x-service-hardening: &service-hardening
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: deal-postgres
|
||||
environment:
|
||||
POSTGRES_DB: deal
|
||||
POSTGRES_USER: deal
|
||||
POSTGRES_PASSWORD: deal_dev_password
|
||||
ports:
|
||||
- "5433:5432" # 5432 может быть занят LeadRadar-стеком
|
||||
volumes:
|
||||
- deal_pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U deal -d deal"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
# MinIO для файлов проектных карточек (Ruling 4, план Task 6; образца-прототипа object_store.py).
|
||||
# Опционально: API работает и без него — LocalFileStorage (data/attachments) выбирается, пока MinIO не
|
||||
# сконфигурирован (Storage__Minio__* / DEAL_MINIO_*). Бакет deal-files создаётся приложением лениво при
|
||||
# первом upload (Ruling 4: EnsureBucket на первом put), отдельный init-контейнер не нужен.
|
||||
# Консоль: http://localhost:9001 (root deal_minio / deal_minio_secret).
|
||||
# Если 9000/9001 заняты LeadRadar-minio — поменяйте на 9100/9101 (и endpoint в конфигурации API).
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: deal-minio
|
||||
environment:
|
||||
MINIO_ROOT_USER: deal_minio
|
||||
MINIO_ROOT_PASSWORD: deal_minio_secret
|
||||
ports:
|
||||
- "9000:9000" # S3 API
|
||||
- "9001:9001" # Web-консоль
|
||||
volumes:
|
||||
- deal_minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
|
||||
# core (deal-api) — HTTP :5080 + gRPC-ингресс telegram-service :5082 (план Task 12/20, Ruling 7/12).
|
||||
# Здесь core поднимается в сквозном gRPC-режиме (UseLocal=false ниже): сервисы этой же сети
|
||||
# вызываются по-настоящему; MlOutbox копится в БД и выгружается MlOutboxFlushScheduler (10 с).
|
||||
# Local-режим (дефолт appsettings.json) остаётся для запуска core с хоста без сервисов.
|
||||
core:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/core/Deal.Api/Dockerfile
|
||||
container_name: deal-core
|
||||
<<: *service-hardening
|
||||
mem_limit: 1g
|
||||
cpus: 1.5
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Development
|
||||
ASPNETCORE_URLS: http://0.0.0.0:5080
|
||||
GRPC_INGRESS_PORT: "5082"
|
||||
ConnectionStrings__DealPostgres: Host=postgres;Port=5432;Database=deal;Username=deal;Password=deal_dev_password
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||||
DEAL_ENCRYPTION_KEY: ${DEAL_ENCRYPTION_KEY:-MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=}
|
||||
# gRPC-режим всего стека (Ruling 6): Local-заглушки кода (appsettings default true)
|
||||
# переключаются на gRPC-клиентов сервисов compose-сети. Хост-запуск core без compose
|
||||
# остаётся на Local (код-дефолт не менялся).
|
||||
Services__Ml__UseLocal: "false"
|
||||
Services__Ai__UseLocal: "false"
|
||||
Services__Telegram__UseLocal: "false"
|
||||
Services__Ml__Endpoint: http://ml-service:5103
|
||||
Services__Ai__Endpoint: http://ai-service:5102
|
||||
Services__Telegram__Endpoint: http://telegram-service:5101
|
||||
Services__Storage__Endpoint: http://storage-service:5104
|
||||
Storage__Minio__Endpoint: minio:9000
|
||||
Storage__Minio__AccessKey: deal_minio
|
||||
Storage__Minio__SecretKey: deal_minio_secret
|
||||
Storage__Minio__Bucket: deal-files
|
||||
Storage__Minio__Secure: "false"
|
||||
ports:
|
||||
- "5080:5080"
|
||||
- "5082:5082" # gRPC-ингресс telegram-service (сервисы ходят на http://core:5082 внутри сети)
|
||||
volumes:
|
||||
- deal_api_data:/app/data # data/encryption.key + data/attachments (Local-фолбэк файлов)
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5082"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 15s
|
||||
|
||||
# telegram-service — gRPC-сервер Telegram (этап 6, Ruling 12; план Task 2). Отдельный процесс,
|
||||
# отдельное sln (src/telegram-service/Deal.Telegram.sln). Команды ядра приходят на localhost:5101
|
||||
# (SERVICES__Telegram__Endpoint — задачи 9–12), исходящий поток — в core-ингресс :5082
|
||||
# (SERVICES__CORE__INGRESS, Ruling 7; внутри compose — http://core:5082; при core на хосте задайте
|
||||
# DEAL_CORE_INGRESS=http://host.docker.internal:5082). Plaintext gRPC без mTLS (Ruling 2 — dev):
|
||||
# защита — общий service-token (DEAL_SERVICE_TOKEN, env хоста и контейнеров). Сессии тенантов —
|
||||
# файлы /data/sessions (Ruling 3), шифруются AES-256-GCM ключом DEAL_TELEGRAM_SESSION_KEY
|
||||
# (fail-closed: без ключа сервис не стартует). Контекст сборки — корень репозитория: csproj
|
||||
# ссылается на src/contracts/Deal.Proto.csproj (общий проект кодогенерации, Task 1).
|
||||
telegram-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/telegram-service/Deal.Telegram/Dockerfile
|
||||
container_name: deal-telegram-service
|
||||
<<: *service-hardening
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5101"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||||
DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:-ZmVkY2JhOTg3NjU0MzIxMGZlZGNiYTk4NzY1NDMyMTA=}
|
||||
DEAL_TELEGRAM_SESSION_DIR: /data/sessions
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082}
|
||||
ports:
|
||||
- "5101:5101"
|
||||
volumes:
|
||||
- deal_tg_sessions:/data/sessions
|
||||
healthcheck:
|
||||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5101"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
# ai-service — gRPC-сервер ИИ-фасада (этап 6, Ruling 12; план Task 4). Отдельный процесс,
|
||||
# отдельное sln (src/ai-service/Deal.Ai.sln). Промпты/классификацию/фильтр/ключи ядро шлёт на
|
||||
# localhost:5102 (SERVICES__Ai__Endpoint — задачи 7/8/15). Plaintext gRPC без mTLS (Ruling 2 —
|
||||
# dev): защита — общий service-token (DEAL_SERVICE_TOKEN, env хоста и контейнеров). Сервис без БД
|
||||
# (Ruling 5): конфиг провайдера (id/base/model/apiKey/api_style) приходит в теле каждого запроса
|
||||
# от ядра, персистентных файлов нет — volume не нужен. Контекст сборки — корень репозитория:
|
||||
# csproj ссылается на src/contracts/Deal.Proto.csproj (общий проект кодогенерации, Task 1).
|
||||
ai-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/ai-service/Deal.Ai/Dockerfile
|
||||
container_name: deal-ai-service
|
||||
<<: *service-hardening
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5102"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
ports:
|
||||
- "5102:5102"
|
||||
healthcheck:
|
||||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5102"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
# ml-service — gRPC-сервер ML (этап 6, Ruling 12; план Task 3). Отдельный процесс, отдельное sln
|
||||
# (src/ml-service/Deal.Ml.sln). Обучение и предсказания ядро шлёт на localhost:5103
|
||||
# (SERVICES__Ml__Endpoint — задачи 5/6/16). Plaintext gRPC без mTLS (Ruling 2 — dev): защита —
|
||||
# общий service-token (DEAL_SERVICE_TOKEN, env хоста и контейнеров). Модели тенантов — файлы
|
||||
# SQLite /data/ml/<tenantId>.sqlite (Ruling 4). Контекст сборки — корень репозитория: csproj
|
||||
# ссылается на src/contracts/Deal.Proto.csproj (общий проект кодогенерации, Task 1).
|
||||
ml-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/ml-service/Deal.Ml/Dockerfile
|
||||
container_name: deal-ml-service
|
||||
<<: *service-hardening
|
||||
mem_limit: 768m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5103"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||||
DEAL_ML_DATA_DIR: /data/ml # файлы моделей data/ml/<tenantId>.sqlite на volume deal_ml_data (Ruling 4/12)
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
ports:
|
||||
- "5103:5103"
|
||||
volumes:
|
||||
- deal_ml_data:/data/ml
|
||||
healthcheck:
|
||||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5103"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
# storage-service — общий gRPC-сервис данных (вложения источников). Отдельный процесс, отдельное sln
|
||||
# (src/storage-service/Deal.Storage.sln). Загрузка/выгрузка/стат/удаление объектов; тип объекта
|
||||
# определяет сервис. Бэкенд — MinIO. Защита — общий service-token (DEAL_SERVICE_TOKEN).
|
||||
storage-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/storage-service/Deal.Storage/Dockerfile
|
||||
container_name: deal-storage-service
|
||||
<<: *service-hardening
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5104"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||||
DEAL_STORAGE_ENDPOINT: minio:9000
|
||||
DEAL_STORAGE_ACCESS_KEY: deal_minio
|
||||
DEAL_STORAGE_SECRET_KEY: deal_minio_secret
|
||||
DEAL_STORAGE_BUCKET: deal-attachments
|
||||
DEAL_STORAGE_SECURE: "false"
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
ports:
|
||||
- "5104:5104"
|
||||
depends_on:
|
||||
- minio
|
||||
healthcheck:
|
||||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5104"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
# Prometheus (профиль observability, этап 12/пакет A) — сбор /metrics всех 4 процессов (:9464)
|
||||
# внутри dev-сети. Подъём: docker compose -f deploy/compose.dev.yml --profile observability up -d.
|
||||
# Конфиг — общий deploy/observability/prometheus.yml (те же имена сервисов и таргеты). UI — 9090.
|
||||
prometheus:
|
||||
image: prom/prometheus:v3.5.0
|
||||
container_name: deal-prometheus
|
||||
profiles: ["observability"]
|
||||
command:
|
||||
- --config.file=/etc/prometheus/prometheus.yml
|
||||
- --storage.tsdb.path=/prometheus
|
||||
- --storage.tsdb.retention.time=15d
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./observability/prometheus-rules.yml:/etc/prometheus/prometheus-rules.yml:ro
|
||||
- deal_prometheus_data:/prometheus
|
||||
|
||||
volumes:
|
||||
deal_pgdata:
|
||||
deal_minio_data:
|
||||
deal_tg_sessions:
|
||||
deal_ml_data:
|
||||
deal_api_data:
|
||||
deal_prometheus_data:
|
||||
# compose.dev.yml — dev-стек Дейла (этап 6, Task 20; Ruling 12).
|
||||
#
|
||||
# Полный стек: postgres + minio (хранилища), три автономных сервиса этапа 6
|
||||
# (telegram-service :5101, ai-service :5102, ml-service :5103, storage-service :5104) и core (deal-api:
|
||||
# HTTP :5080 + gRPC-ингресс :5082).
|
||||
#
|
||||
# Режимы интеграций core — флагами SERVICES__{ML,AI,TELEGRAM}__USELOCAL (Ruling 6):
|
||||
# * этот файл — сквозной gRPC-режим ВСЕГО стека «по-настоящему»: UseLocal=false заданы env
|
||||
# core-сервиса ниже, интеграции ходят в ml/ai/telegram-сервисы этой же compose-сети
|
||||
# (MlOutboxFlushScheduler → TrainBatch, GrpcAiClassifier/GrpcAiTools, GrpcTelegramClient).
|
||||
# Подъём всего стека одной командой: docker compose -f deploy/compose.dev.yml up -d --build
|
||||
# (см. также scripts/dev-smoke.sh — сквозная проверка с авто-очисткой).
|
||||
# * Local-заглушки (дефолт КОДА — appsettings.json, UseLocal:true) сохраняются для запуска core
|
||||
# с хоста БЕЗ сервисов (обычная dev-разработка, curl-приёмки этапов 2–5): поднимаются только
|
||||
# postgres/minio, core — dotnet run из src/core/Deal.Api. Код-дефолт не меняется — режим
|
||||
# переключает этот compose-файл (env ниже).
|
||||
#
|
||||
# Секреты dev (переопределяются .env/экспортом, шаблон ${VAR:-default}):
|
||||
# DEAL_SERVICE_TOKEN — единый service-token всех процессов (metadata gRPC, Ruling 1/2/13)
|
||||
# DEAL_ENCRYPTION_KEY — ключ AES-256-GCM шифрования секретов настроек core (32 байта base64)
|
||||
# DEAL_TELEGRAM_SESSION_KEY — ключ AES-256-GCM файлов сессий telegram-service (32 байта base64)
|
||||
# минио/БД creds — dev-значения ниже (в проде — этап 7, compose-prod + mTLS).
|
||||
#
|
||||
# Core в этом файле — «весь стек в docker» (эндпоинты — имена compose-сети). Альтернатива — core
|
||||
# с хоста (Ruling 12): поднимаются только postgres/minio и (по желанию) сервисы, core запускается
|
||||
# dotnet run, и сервисы ходят в ингресс хоста через DEAL_CORE_INGRESS=http://host.docker.internal:5082
|
||||
# (см. env telegram-service ниже).
|
||||
|
||||
# Общий hardening прикладных сервисов (BL-IMG-HARDEN): read-only root FS + tmpfs /tmp,
|
||||
# запрет новых привилегий и сброс capabilities. Данные — только в именованных volume.
|
||||
x-service-hardening: &service-hardening
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: deal-postgres
|
||||
environment:
|
||||
POSTGRES_DB: deal
|
||||
POSTGRES_USER: deal
|
||||
POSTGRES_PASSWORD: deal_dev_password
|
||||
ports:
|
||||
- "5433:5432" # 5432 может быть занят LeadRadar-стеком
|
||||
volumes:
|
||||
- deal_pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U deal -d deal"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
# MinIO для файлов проектных карточек (Ruling 4, план Task 6; образца-прототипа object_store.py).
|
||||
# Опционально: API работает и без него — LocalFileStorage (data/attachments) выбирается, пока MinIO не
|
||||
# сконфигурирован (Storage__Minio__* / DEAL_MINIO_*). Бакет deal-files создаётся приложением лениво при
|
||||
# первом upload (Ruling 4: EnsureBucket на первом put), отдельный init-контейнер не нужен.
|
||||
# Консоль: http://localhost:9001 (root deal_minio / deal_minio_secret).
|
||||
# Если 9000/9001 заняты LeadRadar-minio — поменяйте на 9100/9101 (и endpoint в конфигурации API).
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: deal-minio
|
||||
environment:
|
||||
MINIO_ROOT_USER: deal_minio
|
||||
MINIO_ROOT_PASSWORD: deal_minio_secret
|
||||
ports:
|
||||
- "9000:9000" # S3 API
|
||||
- "9001:9001" # Web-консоль
|
||||
volumes:
|
||||
- deal_minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
|
||||
# core (deal-api) — HTTP :5080 + gRPC-ингресс telegram-service :5082 (план Task 12/20, Ruling 7/12).
|
||||
# Здесь core поднимается в сквозном gRPC-режиме (UseLocal=false ниже): сервисы этой же сети
|
||||
# вызываются по-настоящему; MlOutbox копится в БД и выгружается MlOutboxFlushScheduler (10 с).
|
||||
# Local-режим (дефолт appsettings.json) остаётся для запуска core с хоста без сервисов.
|
||||
core:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/core/Deal.Api/Dockerfile
|
||||
container_name: deal-core
|
||||
<<: *service-hardening
|
||||
mem_limit: 1g
|
||||
cpus: 1.5
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Development
|
||||
ASPNETCORE_URLS: http://0.0.0.0:5080
|
||||
GRPC_INGRESS_PORT: "5082"
|
||||
ConnectionStrings__DealPostgres: Host=postgres;Port=5432;Database=deal;Username=deal;Password=deal_dev_password
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||||
DEAL_ENCRYPTION_KEY: ${DEAL_ENCRYPTION_KEY:-MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=}
|
||||
# gRPC-режим всего стека (Ruling 6): Local-заглушки кода (appsettings default true)
|
||||
# переключаются на gRPC-клиентов сервисов compose-сети. Хост-запуск core без compose
|
||||
# остаётся на Local (код-дефолт не менялся).
|
||||
Services__Ml__UseLocal: "false"
|
||||
Services__Ai__UseLocal: "false"
|
||||
Services__Telegram__UseLocal: "false"
|
||||
Services__Ml__Endpoint: http://ml-service:5103
|
||||
Services__Ai__Endpoint: http://ai-service:5102
|
||||
Services__Telegram__Endpoint: http://telegram-service:5101
|
||||
Services__Storage__Endpoint: http://storage-service:5104
|
||||
Storage__Minio__Endpoint: minio:9000
|
||||
Storage__Minio__AccessKey: deal_minio
|
||||
Storage__Minio__SecretKey: deal_minio_secret
|
||||
Storage__Minio__Bucket: deal-files
|
||||
Storage__Minio__Secure: "false"
|
||||
ports:
|
||||
- "5080:5080"
|
||||
- "5082:5082" # gRPC-ингресс telegram-service (сервисы ходят на http://core:5082 внутри сети)
|
||||
volumes:
|
||||
- deal_api_data:/app/data # data/encryption.key + data/attachments (Local-фолбэк файлов)
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5082"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 15s
|
||||
|
||||
# telegram-service — gRPC-сервер Telegram (этап 6, Ruling 12; план Task 2). Отдельный процесс,
|
||||
# отдельное sln (src/telegram-service/Deal.Telegram.sln). Команды ядра приходят на localhost:5101
|
||||
# (SERVICES__Telegram__Endpoint — задачи 9–12), исходящий поток — в core-ингресс :5082
|
||||
# (SERVICES__CORE__INGRESS, Ruling 7; внутри compose — http://core:5082; при core на хосте задайте
|
||||
# DEAL_CORE_INGRESS=http://host.docker.internal:5082). Plaintext gRPC без mTLS (Ruling 2 — dev):
|
||||
# защита — общий service-token (DEAL_SERVICE_TOKEN, env хоста и контейнеров). Сессии тенантов —
|
||||
# файлы /data/sessions (Ruling 3), шифруются AES-256-GCM ключом DEAL_TELEGRAM_SESSION_KEY
|
||||
# (fail-closed: без ключа сервис не стартует). Контекст сборки — корень репозитория: csproj
|
||||
# ссылается на src/contracts/Deal.Proto.csproj (общий проект кодогенерации, Task 1).
|
||||
telegram-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/telegram-service/Deal.Telegram/Dockerfile
|
||||
container_name: deal-telegram-service
|
||||
<<: *service-hardening
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5101"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||||
DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:-ZmVkY2JhOTg3NjU0MzIxMGZlZGNiYTk4NzY1NDMyMTA=}
|
||||
DEAL_TELEGRAM_SESSION_DIR: /data/sessions
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082}
|
||||
ports:
|
||||
- "5101:5101"
|
||||
volumes:
|
||||
- deal_tg_sessions:/data/sessions
|
||||
healthcheck:
|
||||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5101"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
# ai-service — gRPC-сервер ИИ-фасада (этап 6, Ruling 12; план Task 4). Отдельный процесс,
|
||||
# отдельное sln (src/ai-service/Deal.Ai.sln). Промпты/классификацию/фильтр/ключи ядро шлёт на
|
||||
# localhost:5102 (SERVICES__Ai__Endpoint — задачи 7/8/15). Plaintext gRPC без mTLS (Ruling 2 —
|
||||
# dev): защита — общий service-token (DEAL_SERVICE_TOKEN, env хоста и контейнеров). Сервис без БД
|
||||
# (Ruling 5): конфиг провайдера (id/base/model/apiKey/api_style) приходит в теле каждого запроса
|
||||
# от ядра, персистентных файлов нет — volume не нужен. Контекст сборки — корень репозитория:
|
||||
# csproj ссылается на src/contracts/Deal.Proto.csproj (общий проект кодогенерации, Task 1).
|
||||
ai-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/ai-service/Deal.Ai/Dockerfile
|
||||
container_name: deal-ai-service
|
||||
<<: *service-hardening
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5102"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
ports:
|
||||
- "5102:5102"
|
||||
healthcheck:
|
||||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5102"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
# ml-service — gRPC-сервер ML (этап 6, Ruling 12; план Task 3). Отдельный процесс, отдельное sln
|
||||
# (src/ml-service/Deal.Ml.sln). Обучение и предсказания ядро шлёт на localhost:5103
|
||||
# (SERVICES__Ml__Endpoint — задачи 5/6/16). Plaintext gRPC без mTLS (Ruling 2 — dev): защита —
|
||||
# общий service-token (DEAL_SERVICE_TOKEN, env хоста и контейнеров). Модели тенантов — файлы
|
||||
# SQLite /data/ml/<tenantId>.sqlite (Ruling 4). Контекст сборки — корень репозитория: csproj
|
||||
# ссылается на src/contracts/Deal.Proto.csproj (общий проект кодогенерации, Task 1).
|
||||
ml-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/ml-service/Deal.Ml/Dockerfile
|
||||
container_name: deal-ml-service
|
||||
<<: *service-hardening
|
||||
mem_limit: 768m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5103"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||||
DEAL_ML_DATA_DIR: /data/ml # файлы моделей data/ml/<tenantId>.sqlite на volume deal_ml_data (Ruling 4/12)
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
ports:
|
||||
- "5103:5103"
|
||||
volumes:
|
||||
- deal_ml_data:/data/ml
|
||||
healthcheck:
|
||||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5103"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
# storage-service — общий gRPC-сервис данных (вложения источников). Отдельный процесс, отдельное sln
|
||||
# (src/storage-service/Deal.Storage.sln). Загрузка/выгрузка/стат/удаление объектов; тип объекта
|
||||
# определяет сервис. Бэкенд — MinIO. Защита — общий service-token (DEAL_SERVICE_TOKEN).
|
||||
storage-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/storage-service/Deal.Storage/Dockerfile
|
||||
container_name: deal-storage-service
|
||||
<<: *service-hardening
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5104"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||||
DEAL_STORAGE_ENDPOINT: minio:9000
|
||||
DEAL_STORAGE_ACCESS_KEY: deal_minio
|
||||
DEAL_STORAGE_SECRET_KEY: deal_minio_secret
|
||||
DEAL_STORAGE_BUCKET: deal-attachments
|
||||
DEAL_STORAGE_SECURE: "false"
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
ports:
|
||||
- "5104:5104"
|
||||
depends_on:
|
||||
- minio
|
||||
healthcheck:
|
||||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5104"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
# Prometheus (профиль observability, этап 12/пакет A) — сбор /metrics всех 4 процессов (:9464)
|
||||
# внутри dev-сети. Подъём: docker compose -f deploy/compose.dev.yml --profile observability up -d.
|
||||
# Конфиг — общий deploy/observability/prometheus.yml (те же имена сервисов и таргеты). UI — 9090.
|
||||
prometheus:
|
||||
image: prom/prometheus:v3.5.0
|
||||
container_name: deal-prometheus
|
||||
profiles: ["observability"]
|
||||
command:
|
||||
- --config.file=/etc/prometheus/prometheus.yml
|
||||
- --storage.tsdb.path=/prometheus
|
||||
- --storage.tsdb.retention.time=15d
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./observability/prometheus-rules.yml:/etc/prometheus/prometheus-rules.yml:ro
|
||||
- deal_prometheus_data:/prometheus
|
||||
|
||||
volumes:
|
||||
deal_pgdata:
|
||||
deal_minio_data:
|
||||
deal_tg_sessions:
|
||||
deal_ml_data:
|
||||
deal_api_data:
|
||||
deal_prometheus_data:
|
||||
|
||||
+350
-350
@@ -1,350 +1,350 @@
|
||||
# compose.prod.yml — PROD-стек «Дейла» (этап 7, Ruling 6/7/9; план Task 14).
|
||||
#
|
||||
# ⚠ НЕ поднимать на dev-машине вместо compose.dev.yml: это production-конфигурация с
|
||||
# fail-fast на секреты (см. ниже) и без dev-дефолтов.
|
||||
#
|
||||
# Состав (всё в одной внутренней сети compose, наружу — ТОЛЬКО caddy :80/:443):
|
||||
# postgres, minio — хранилища БЕЗ host-портов (volume'ы);
|
||||
# core (:5080 HTTP + :5082 gRPC-ингресс), telegram-service (:5101), ai-service (:5102),
|
||||
# ml-service (:5103) — процессы «Дейла»; mTLS-транспорт — по env Ruling 6 (см. ниже);
|
||||
# caddy — edge: TLS-терминация, статика фронта, reverse_proxy /api → core.
|
||||
# loki/promtail/grafana/prometheus — observability (Ruling 7; метрики — этап 12, пакет A): ПРОФИЛЬ
|
||||
# `observability` — поднимается только: docker compose --profile observability up -d
|
||||
# (или ... up -d --profile observability). Prometheus scrape'ит /metrics
|
||||
# (порт 9464) всех 4 процессов; Grafana — логи (Loki) и метрики (Prometheus).
|
||||
#
|
||||
# Секреты — ТОЛЬКО из env: шаблон deploy/.env.prod.example → скопируйте в deploy/.env.prod,
|
||||
# заполните значения и запускайте с --env-file:
|
||||
# cp deploy/.env.prod.example deploy/.env.prod # и заполнить (без дефолтных паролей!)
|
||||
# docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml up -d --build
|
||||
# docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml \
|
||||
# --profile observability up -d --build # + Loki/Promtail/Grafana/Prometheus
|
||||
# Проверка без движка: docker compose --env-file ... -f deploy/compose.prod.yml config
|
||||
# Ключевые обязательные (без значения `config` упадёт с подсказкой): DEAL_PG_PASSWORD,
|
||||
# DEAL_ENCRYPTION_KEY, DEAL_SERVICE_TOKEN, DEAL_TELEGRAM_SESSION_KEY, MINIO_ROOT_USER,
|
||||
# MINIO_ROOT_PASSWORD, DEAL_OPERATOR_LOGIN/DEAL_OPERATOR_PASSWORD (пустые — оператор не создаётся,
|
||||
# см. Ruling 1), DEAL_ALLOWED_ORIGINS.
|
||||
#
|
||||
# PROD-флаги кода (Ruling 5/9/10): RateLimit__Enabled=true (лимиты/защита входа), куки
|
||||
# Cookies__Secure/OperatorCookies__Secure=true (TLS терминирует caddy), ForwardedHeaders__Enabled=true
|
||||
# (доверенный прокси — caddy; KnownNetworks — приватные docker-сети хоста: core наружу не публикуется,
|
||||
# единственный вход в него — caddy), Security__AllowedOrigins — origin фронта.
|
||||
#
|
||||
# mTLS внутреннего gRPC (Ruling 6, Task 13/14): DEAL_MTLS_ENABLED=0 — plaintext + service-token
|
||||
# (как dev, но внутри изолированной сети); DEAL_MTLS_ENABLED=1 — Kestrel сервисов/ингресса требует
|
||||
# клиентский сертификат. Сертификаты: deploy/certs (scripts/mtls-certs.sh) монтируются в
|
||||
# /etc/deal/certs. ВАЖНО при включении mTLS:
|
||||
# * схемы внутренних endpoint'ов поменять на https:// (DEAL_ML_ENDPOINT/DEAL_AI_ENDPOINT/
|
||||
# DEAL_TELEGRAM_ENDPOINT/DEAL_CORE_INGRESS в .env.prod);
|
||||
# * healthcheck'и ниже сами переключаются на TLS-пробу grpc_health_probe (клиентский сертификат из
|
||||
# /etc/deal/certs; PEM-артефакты deal-client.crt/.key генерирует scripts/mtls-certs.sh);
|
||||
# * DEAL_MTLS_CERT_PASSWORD — пароль PFX (серверных и клиентского).
|
||||
#
|
||||
# Логи (Ruling 7): процессы пишут Serilog JSON в stdout (docker-логи) + rolling-файл
|
||||
# data/logs/deal-<процесс>.json в контейнере. У core /app/data — volume deal_api_data (файлы-логи
|
||||
# переживают пересоздание); у сервисов канал наблюдаемости — docker-логи → promtail (файлы сервисов
|
||||
# эфемерны). Уровень — env DEAL_LOG_LEVEL.
|
||||
#
|
||||
# Frontend: статика монтируется из ../src/frontend/dist (СОБРАТЬ ДО ПОДЪЁМА: cd src/frontend &&
|
||||
# npm run build). Конфигурация caddy — deploy/caddy/Caddyfile (см. его шапку про домен/TLS).
|
||||
|
||||
# Общий hardening прикладных сервисов (BL-IMG-HARDEN): read-only root FS + tmpfs /tmp,
|
||||
# запрет новых привилегий и сброс capabilities. Данные/логи — только в именованных volume или tmpfs.
|
||||
x-service-hardening: &service-hardening
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: deal
|
||||
POSTGRES_USER: deal
|
||||
POSTGRES_PASSWORD: ${DEAL_PG_PASSWORD:?DEAL_PG_PASSWORD не задан (пароль Postgres deal)}
|
||||
volumes:
|
||||
- deal_pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U deal -d deal"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
# MinIO (файлы проектных карточек; бакет deal-files создаётся лениво при первом upload, Ruling 4).
|
||||
# БЕЗ host-портов: наружу файлы ходят только через core→caddy. Healthcheck нет: minio-образ не несёт
|
||||
# curl/mc; на core падение minio = фолбэк LocalFileStorage (данные — в volume, подъём лечит).
|
||||
minio:
|
||||
# Тег зафиксирован (как остальные образы); при обновлении — свежий RELEASE-тег из quay.io/minio/minio.
|
||||
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER не задан}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD не задан}
|
||||
volumes:
|
||||
- deal_minio_data:/data
|
||||
command: server /data
|
||||
restart: unless-stopped
|
||||
|
||||
# core (deal-api): HTTP :5080 + gRPC-ингресс telegram-service :5082. Сквозной gRPC-режим
|
||||
# (UseLocal=false), как compose.dev.yml; rate limiting/куки-Secure/прокси-заголовки — PROD (см. шапку).
|
||||
core:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/core/Deal.Api/Dockerfile
|
||||
<<: *service-hardening
|
||||
mem_limit: 1g
|
||||
cpus: 1.5
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
ASPNETCORE_URLS: http://0.0.0.0:5080
|
||||
GRPC_INGRESS_PORT: "5082"
|
||||
ConnectionStrings__DealPostgres: "Host=postgres;Port=5432;Database=deal;Username=deal;Password=${DEAL_PG_PASSWORD:?DEAL_PG_PASSWORD не задан}"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан (service-token gRPC)}
|
||||
DEAL_ENCRYPTION_KEY: ${DEAL_ENCRYPTION_KEY:?DEAL_ENCRYPTION_KEY не задан (AES-256-GCM, 32 байта base64)}
|
||||
DEAL_OPERATOR_LOGIN: ${DEAL_OPERATOR_LOGIN:-}
|
||||
DEAL_OPERATOR_PASSWORD: ${DEAL_OPERATOR_PASSWORD:-}
|
||||
DEAL_DEFAULT_AI_BUDGET: ${DEAL_DEFAULT_AI_BUDGET:-}
|
||||
# Безопасность PROD (Rulings 5/9/10): лимиты вкл., куки Secure, доверенный прокси (caddy).
|
||||
RateLimit__Enabled: "true"
|
||||
Cookies__Secure: "true"
|
||||
OperatorCookies__Secure: "true"
|
||||
ForwardedHeaders__Enabled: "true"
|
||||
# Доверенные подсети прокси: приватные docker-сети хоста (caddy — единственный вход в core,
|
||||
# host-портов у core нет). Свою подсеть можно уточнить: docker network inspect <сеть>.
|
||||
ForwardedHeaders__KnownNetworks__0: 172.16.0.0/12
|
||||
Security__AllowedOrigins: ${DEAL_ALLOWED_ORIGINS:?DEAL_ALLOWED_ORIGINS не задан (origin фронта, напр. https://deal.example)}
|
||||
# Интеграции — gRPC-режим всего стека (как dev); схемы endpoint'ов — http:// (mTLS off) либо
|
||||
# https:// при DEAL_MTLS_ENABLED=1 (см. .env.prod.example).
|
||||
Services__Ml__UseLocal: "false"
|
||||
Services__Ml__Endpoint: ${DEAL_ML_ENDPOINT:-http://ml-service:5103}
|
||||
Services__Ai__UseLocal: "false"
|
||||
Services__Ai__Endpoint: ${DEAL_AI_ENDPOINT:-http://ai-service:5102}
|
||||
Services__Telegram__UseLocal: "false"
|
||||
Services__Telegram__Endpoint: ${DEAL_TELEGRAM_ENDPOINT:-http://telegram-service:5101}
|
||||
# Файлы — MinIO (внутренний http; TLS minio — вне этапа, при желании Storage__Minio__Secure=true
|
||||
# + endpoint https и сертификаты).
|
||||
Storage__Minio__Endpoint: minio:9000
|
||||
Storage__Minio__AccessKey: ${MINIO_ROOT_USER:?MINIO_ROOT_USER не задан}
|
||||
Storage__Minio__SecretKey: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD не задан}
|
||||
Storage__Minio__Bucket: deal-files
|
||||
Storage__Minio__Secure: "false"
|
||||
# mTLS внутреннего gRPC (Ruling 6; пути — /etc/deal/certs, см. volume ниже).
|
||||
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
|
||||
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
|
||||
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/core-server.pfx
|
||||
DEAL_MTLS_SERVER_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
DEAL_MTLS_CLIENT_CERT_PFX: /etc/deal/certs/deal-client.pfx
|
||||
DEAL_MTLS_CLIENT_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
volumes:
|
||||
# data/: ключ шифрования (или DEAL_ENCRYPTION_KEY), LocalFileStorage-фолбэк, логи data/logs.
|
||||
- deal_api_data:/app/data
|
||||
# deploy/certs (scripts/mtls-certs.sh): нужны только при DEAL_MTLS_ENABLED=1.
|
||||
- ${DEAL_CERTS_DIR:-./certs}:/etc/deal/certs:ro
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
# gRPC-health ингресса :5082 (health освобождён от service-token). Под mTLS — TLS-проба
|
||||
# с клиентским сертификатом deal-client и CA (PEM-артефакты генерирует scripts/mtls-certs.sh).
|
||||
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5082 -tls -tls-ca-cert=/etc/deal/certs/ca.pem -tls-client-cert=/etc/deal/certs/deal-client.crt -tls-client-key=/etc/deal/certs/deal-client.key -tls-server-name=localhost; else /bin/grpc_health_probe -addr=localhost:5082; fi"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 15s
|
||||
restart: unless-stopped
|
||||
|
||||
# telegram-service — gRPC-сервер Telegram (:5101; plaintext либо mTLS — как core). Сессии —
|
||||
# /data/sessions (volume deal_tg_sessions, AES-GCM ключом DEAL_TELEGRAM_SESSION_KEY, fail-closed).
|
||||
telegram-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/telegram-service/Deal.Telegram/Dockerfile
|
||||
<<: *service-hardening
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5101"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
|
||||
DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:?DEAL_TELEGRAM_SESSION_KEY не задан (ключ AES-GCM сессий)}
|
||||
DEAL_TELEGRAM_SESSION_DIR: /data/sessions
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082}
|
||||
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
|
||||
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
|
||||
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/telegram-service-server.pfx
|
||||
DEAL_MTLS_SERVER_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
DEAL_MTLS_CLIENT_CERT_PFX: /etc/deal/certs/deal-client.pfx
|
||||
DEAL_MTLS_CLIENT_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
volumes:
|
||||
- deal_tg_sessions:/data/sessions
|
||||
- ${DEAL_CERTS_DIR:-./certs}:/etc/deal/certs:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5101 -tls -tls-ca-cert=/etc/deal/certs/ca.pem -tls-client-cert=/etc/deal/certs/deal-client.crt -tls-client-key=/etc/deal/certs/deal-client.key -tls-server-name=localhost; else /bin/grpc_health_probe -addr=localhost:5101; fi"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
# ai-service — gRPC-сервер ИИ-фасада (:5102). Без БД и volume'ов (конфиг провайдера — в теле RPC).
|
||||
ai-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/ai-service/Deal.Ai/Dockerfile
|
||||
<<: *service-hardening
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5102"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
|
||||
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
|
||||
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/ai-service-server.pfx
|
||||
DEAL_MTLS_SERVER_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
DEAL_MTLS_CLIENT_CERT_PFX: /etc/deal/certs/deal-client.pfx
|
||||
DEAL_MTLS_CLIENT_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
volumes:
|
||||
- ${DEAL_CERTS_DIR:-./certs}:/etc/deal/certs:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5102 -tls -tls-ca-cert=/etc/deal/certs/ca.pem -tls-client-cert=/etc/deal/certs/deal-client.crt -tls-client-key=/etc/deal/certs/deal-client.key -tls-server-name=localhost; else /bin/grpc_health_probe -addr=localhost:5102; fi"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
# ml-service — gRPC-сервер ML (:5103). Модели — /data/ml (volume deal_ml_data, SQLite на тенанта).
|
||||
ml-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/ml-service/Deal.Ml/Dockerfile
|
||||
<<: *service-hardening
|
||||
mem_limit: 768m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5103"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
|
||||
DEAL_ML_DATA_DIR: /data/ml
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
|
||||
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
|
||||
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/ml-service-server.pfx
|
||||
DEAL_MTLS_SERVER_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
DEAL_MTLS_CLIENT_CERT_PFX: /etc/deal/certs/deal-client.pfx
|
||||
DEAL_MTLS_CLIENT_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
volumes:
|
||||
- deal_ml_data:/data/ml
|
||||
- ${DEAL_CERTS_DIR:-./certs}:/etc/deal/certs:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5103 -tls -tls-ca-cert=/etc/deal/certs/ca.pem -tls-client-cert=/etc/deal/certs/deal-client.crt -tls-client-key=/etc/deal/certs/deal-client.key -tls-server-name=localhost; else /bin/grpc_health_probe -addr=localhost:5103; fi"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
# caddy — edge: наружу только :80/:443. TLS — плейсхолдер tls internal (см. Caddyfile: домен,
|
||||
# реальный сертификат/Cloudflare, CSP/HSTS). Статика — ../src/frontend/dist (СОБРАТЬ ДО up).
|
||||
caddy:
|
||||
image: caddy:2.9.1
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ../src/frontend/dist:/srv:ro
|
||||
# /data — локальный CA и сертификаты Caddy (переживают пересоздание контейнера).
|
||||
- deal_caddy_data:/data
|
||||
- deal_caddy_config:/config
|
||||
depends_on:
|
||||
core:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
# ── Observability (Ruling 7/9; метрики — этап 12, пакет A) — ПРОФИЛЬ: --profile observability ──
|
||||
# Логи: docker-логи (stdout контейнеров) → promtail → loki → grafana (127.0.0.1:3001).
|
||||
# Метрики: /metrics процессов (:9464) → prometheus (127.0.0.1:9090) → grafana (датасорс uid
|
||||
# `prometheus`). Доступ — оператору по SSH-туннелю; наружу grafana/prometheus НЕ публикуются.
|
||||
# Версии образов — фиксированные (при подъёме обновите до актуальных patch-релизов).
|
||||
|
||||
loki:
|
||||
image: grafana/loki:3.4.2
|
||||
profiles: ["observability"]
|
||||
command: -config.file=/etc/loki/loki.yml
|
||||
volumes:
|
||||
- ./observability/loki.yml:/etc/loki/loki.yml:ro
|
||||
- deal_loki_data:/loki
|
||||
restart: unless-stopped
|
||||
|
||||
# Prometheus — сбор метрик Deal-процессов (этап 12, пакет A): scrape /metrics каждого процесса
|
||||
# (:9464, HTTP/1.1) внутри compose-сети. Конфиг — deploy/observability/prometheus.yml (таргеты
|
||||
# core/telegram-service/ai-service/ml-service). UI — только loopback 127.0.0.1:9090 (оператору по
|
||||
# SSH-туннелю, как Grafana); наружу порт не публикуется. Retention 15 суток (volume deal_prometheus_data).
|
||||
prometheus:
|
||||
image: prom/prometheus:v3.5.0
|
||||
profiles: ["observability"]
|
||||
command:
|
||||
- --config.file=/etc/prometheus/prometheus.yml
|
||||
- --storage.tsdb.path=/prometheus
|
||||
- --storage.tsdb.retention.time=15d
|
||||
ports:
|
||||
# Только loopback хоста — наружу не публикуется (Ruling 9).
|
||||
- "127.0.0.1:9090:9090"
|
||||
volumes:
|
||||
- ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./observability/prometheus-rules.yml:/etc/prometheus/prometheus-rules.yml:ro
|
||||
- deal_prometheus_data:/prometheus
|
||||
restart: unless-stopped
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:3.4.2
|
||||
profiles: ["observability"]
|
||||
command: -config.file=/etc/promtail/promtail.yml
|
||||
volumes:
|
||||
- ./observability/promtail.yml:/etc/promtail/promtail.yml:ro
|
||||
# docker.sock — чтение docker-логов всех контейнеров (docker_sd). Rootless-докер: путь сокета
|
||||
# свой (напр. /run/user/<uid>/docker.sock) — поправьте монтирование и promtail.yml.
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- deal_promtail_data:/var/lib/promtail
|
||||
depends_on:
|
||||
loki:
|
||||
condition: service_started
|
||||
restart: unless-stopped
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:11.5.2
|
||||
profiles: ["observability"]
|
||||
environment:
|
||||
# Пароль задайте в .env.prod перед подъёмом профиля (пусто — grafana не получит admin-пароль).
|
||||
GF_SECURITY_ADMIN_USER: ${DEAL_GRAFANA_ADMIN_USER:-admin}
|
||||
GF_SECURITY_ADMIN_PASSWORD: ${DEAL_GRAFANA_ADMIN_PASSWORD:-}
|
||||
GF_USERS_ALLOW_SIGN_UP: "false"
|
||||
GF_AUTH_ANONYMOUS_ENABLED: "false"
|
||||
ports:
|
||||
# Только loopback хоста — оператору по SSH-туннелю (Ruling 9); наружу не публикуется.
|
||||
- "127.0.0.1:3001:3000"
|
||||
volumes:
|
||||
- ./observability/grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- ./observability/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
- deal_grafana_data:/var/lib/grafana
|
||||
depends_on:
|
||||
loki:
|
||||
condition: service_started
|
||||
prometheus:
|
||||
condition: service_started
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
deal_pgdata:
|
||||
deal_minio_data:
|
||||
deal_tg_sessions:
|
||||
deal_ml_data:
|
||||
deal_api_data:
|
||||
deal_caddy_data:
|
||||
deal_caddy_config:
|
||||
deal_loki_data:
|
||||
deal_promtail_data:
|
||||
deal_grafana_data:
|
||||
deal_prometheus_data:
|
||||
# compose.prod.yml — PROD-стек «Дейла» (этап 7, Ruling 6/7/9; план Task 14).
|
||||
#
|
||||
# ⚠ НЕ поднимать на dev-машине вместо compose.dev.yml: это production-конфигурация с
|
||||
# fail-fast на секреты (см. ниже) и без dev-дефолтов.
|
||||
#
|
||||
# Состав (всё в одной внутренней сети compose, наружу — ТОЛЬКО caddy :80/:443):
|
||||
# postgres, minio — хранилища БЕЗ host-портов (volume'ы);
|
||||
# core (:5080 HTTP + :5082 gRPC-ингресс), telegram-service (:5101), ai-service (:5102),
|
||||
# ml-service (:5103) — процессы «Дейла»; mTLS-транспорт — по env Ruling 6 (см. ниже);
|
||||
# caddy — edge: TLS-терминация, статика фронта, reverse_proxy /api → core.
|
||||
# loki/promtail/grafana/prometheus — observability (Ruling 7; метрики — этап 12, пакет A): ПРОФИЛЬ
|
||||
# `observability` — поднимается только: docker compose --profile observability up -d
|
||||
# (или ... up -d --profile observability). Prometheus scrape'ит /metrics
|
||||
# (порт 9464) всех 4 процессов; Grafana — логи (Loki) и метрики (Prometheus).
|
||||
#
|
||||
# Секреты — ТОЛЬКО из env: шаблон deploy/.env.prod.example → скопируйте в deploy/.env.prod,
|
||||
# заполните значения и запускайте с --env-file:
|
||||
# cp deploy/.env.prod.example deploy/.env.prod # и заполнить (без дефолтных паролей!)
|
||||
# docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml up -d --build
|
||||
# docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml \
|
||||
# --profile observability up -d --build # + Loki/Promtail/Grafana/Prometheus
|
||||
# Проверка без движка: docker compose --env-file ... -f deploy/compose.prod.yml config
|
||||
# Ключевые обязательные (без значения `config` упадёт с подсказкой): DEAL_PG_PASSWORD,
|
||||
# DEAL_ENCRYPTION_KEY, DEAL_SERVICE_TOKEN, DEAL_TELEGRAM_SESSION_KEY, MINIO_ROOT_USER,
|
||||
# MINIO_ROOT_PASSWORD, DEAL_OPERATOR_LOGIN/DEAL_OPERATOR_PASSWORD (пустые — оператор не создаётся,
|
||||
# см. Ruling 1), DEAL_ALLOWED_ORIGINS.
|
||||
#
|
||||
# PROD-флаги кода (Ruling 5/9/10): RateLimit__Enabled=true (лимиты/защита входа), куки
|
||||
# Cookies__Secure/OperatorCookies__Secure=true (TLS терминирует caddy), ForwardedHeaders__Enabled=true
|
||||
# (доверенный прокси — caddy; KnownNetworks — приватные docker-сети хоста: core наружу не публикуется,
|
||||
# единственный вход в него — caddy), Security__AllowedOrigins — origin фронта.
|
||||
#
|
||||
# mTLS внутреннего gRPC (Ruling 6, Task 13/14): DEAL_MTLS_ENABLED=0 — plaintext + service-token
|
||||
# (как dev, но внутри изолированной сети); DEAL_MTLS_ENABLED=1 — Kestrel сервисов/ингресса требует
|
||||
# клиентский сертификат. Сертификаты: deploy/certs (scripts/mtls-certs.sh) монтируются в
|
||||
# /etc/deal/certs. ВАЖНО при включении mTLS:
|
||||
# * схемы внутренних endpoint'ов поменять на https:// (DEAL_ML_ENDPOINT/DEAL_AI_ENDPOINT/
|
||||
# DEAL_TELEGRAM_ENDPOINT/DEAL_CORE_INGRESS в .env.prod);
|
||||
# * healthcheck'и ниже сами переключаются на TLS-пробу grpc_health_probe (клиентский сертификат из
|
||||
# /etc/deal/certs; PEM-артефакты deal-client.crt/.key генерирует scripts/mtls-certs.sh);
|
||||
# * DEAL_MTLS_CERT_PASSWORD — пароль PFX (серверных и клиентского).
|
||||
#
|
||||
# Логи (Ruling 7): процессы пишут Serilog JSON в stdout (docker-логи) + rolling-файл
|
||||
# data/logs/deal-<процесс>.json в контейнере. У core /app/data — volume deal_api_data (файлы-логи
|
||||
# переживают пересоздание); у сервисов канал наблюдаемости — docker-логи → promtail (файлы сервисов
|
||||
# эфемерны). Уровень — env DEAL_LOG_LEVEL.
|
||||
#
|
||||
# Frontend: статика монтируется из ../src/frontend/dist (СОБРАТЬ ДО ПОДЪЁМА: cd src/frontend &&
|
||||
# npm run build). Конфигурация caddy — deploy/caddy/Caddyfile (см. его шапку про домен/TLS).
|
||||
|
||||
# Общий hardening прикладных сервисов (BL-IMG-HARDEN): read-only root FS + tmpfs /tmp,
|
||||
# запрет новых привилегий и сброс capabilities. Данные/логи — только в именованных volume или tmpfs.
|
||||
x-service-hardening: &service-hardening
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: deal
|
||||
POSTGRES_USER: deal
|
||||
POSTGRES_PASSWORD: ${DEAL_PG_PASSWORD:?DEAL_PG_PASSWORD не задан (пароль Postgres deal)}
|
||||
volumes:
|
||||
- deal_pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U deal -d deal"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
# MinIO (файлы проектных карточек; бакет deal-files создаётся лениво при первом upload, Ruling 4).
|
||||
# БЕЗ host-портов: наружу файлы ходят только через core→caddy. Healthcheck нет: minio-образ не несёт
|
||||
# curl/mc; на core падение minio = фолбэк LocalFileStorage (данные — в volume, подъём лечит).
|
||||
minio:
|
||||
# Тег зафиксирован (как остальные образы); при обновлении — свежий RELEASE-тег из quay.io/minio/minio.
|
||||
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER не задан}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD не задан}
|
||||
volumes:
|
||||
- deal_minio_data:/data
|
||||
command: server /data
|
||||
restart: unless-stopped
|
||||
|
||||
# core (deal-api): HTTP :5080 + gRPC-ингресс telegram-service :5082. Сквозной gRPC-режим
|
||||
# (UseLocal=false), как compose.dev.yml; rate limiting/куки-Secure/прокси-заголовки — PROD (см. шапку).
|
||||
core:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/core/Deal.Api/Dockerfile
|
||||
<<: *service-hardening
|
||||
mem_limit: 1g
|
||||
cpus: 1.5
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
ASPNETCORE_URLS: http://0.0.0.0:5080
|
||||
GRPC_INGRESS_PORT: "5082"
|
||||
ConnectionStrings__DealPostgres: "Host=postgres;Port=5432;Database=deal;Username=deal;Password=${DEAL_PG_PASSWORD:?DEAL_PG_PASSWORD не задан}"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан (service-token gRPC)}
|
||||
DEAL_ENCRYPTION_KEY: ${DEAL_ENCRYPTION_KEY:?DEAL_ENCRYPTION_KEY не задан (AES-256-GCM, 32 байта base64)}
|
||||
DEAL_OPERATOR_LOGIN: ${DEAL_OPERATOR_LOGIN:-}
|
||||
DEAL_OPERATOR_PASSWORD: ${DEAL_OPERATOR_PASSWORD:-}
|
||||
DEAL_DEFAULT_AI_BUDGET: ${DEAL_DEFAULT_AI_BUDGET:-}
|
||||
# Безопасность PROD (Rulings 5/9/10): лимиты вкл., куки Secure, доверенный прокси (caddy).
|
||||
RateLimit__Enabled: "true"
|
||||
Cookies__Secure: "true"
|
||||
OperatorCookies__Secure: "true"
|
||||
ForwardedHeaders__Enabled: "true"
|
||||
# Доверенные подсети прокси: приватные docker-сети хоста (caddy — единственный вход в core,
|
||||
# host-портов у core нет). Свою подсеть можно уточнить: docker network inspect <сеть>.
|
||||
ForwardedHeaders__KnownNetworks__0: 172.16.0.0/12
|
||||
Security__AllowedOrigins: ${DEAL_ALLOWED_ORIGINS:?DEAL_ALLOWED_ORIGINS не задан (origin фронта, напр. https://deal.example)}
|
||||
# Интеграции — gRPC-режим всего стека (как dev); схемы endpoint'ов — http:// (mTLS off) либо
|
||||
# https:// при DEAL_MTLS_ENABLED=1 (см. .env.prod.example).
|
||||
Services__Ml__UseLocal: "false"
|
||||
Services__Ml__Endpoint: ${DEAL_ML_ENDPOINT:-http://ml-service:5103}
|
||||
Services__Ai__UseLocal: "false"
|
||||
Services__Ai__Endpoint: ${DEAL_AI_ENDPOINT:-http://ai-service:5102}
|
||||
Services__Telegram__UseLocal: "false"
|
||||
Services__Telegram__Endpoint: ${DEAL_TELEGRAM_ENDPOINT:-http://telegram-service:5101}
|
||||
# Файлы — MinIO (внутренний http; TLS minio — вне этапа, при желании Storage__Minio__Secure=true
|
||||
# + endpoint https и сертификаты).
|
||||
Storage__Minio__Endpoint: minio:9000
|
||||
Storage__Minio__AccessKey: ${MINIO_ROOT_USER:?MINIO_ROOT_USER не задан}
|
||||
Storage__Minio__SecretKey: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD не задан}
|
||||
Storage__Minio__Bucket: deal-files
|
||||
Storage__Minio__Secure: "false"
|
||||
# mTLS внутреннего gRPC (Ruling 6; пути — /etc/deal/certs, см. volume ниже).
|
||||
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
|
||||
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
|
||||
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/core-server.pfx
|
||||
DEAL_MTLS_SERVER_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
DEAL_MTLS_CLIENT_CERT_PFX: /etc/deal/certs/deal-client.pfx
|
||||
DEAL_MTLS_CLIENT_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
volumes:
|
||||
# data/: ключ шифрования (или DEAL_ENCRYPTION_KEY), LocalFileStorage-фолбэк, логи data/logs.
|
||||
- deal_api_data:/app/data
|
||||
# deploy/certs (scripts/mtls-certs.sh): нужны только при DEAL_MTLS_ENABLED=1.
|
||||
- ${DEAL_CERTS_DIR:-./certs}:/etc/deal/certs:ro
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
# gRPC-health ингресса :5082 (health освобождён от service-token). Под mTLS — TLS-проба
|
||||
# с клиентским сертификатом deal-client и CA (PEM-артефакты генерирует scripts/mtls-certs.sh).
|
||||
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5082 -tls -tls-ca-cert=/etc/deal/certs/ca.pem -tls-client-cert=/etc/deal/certs/deal-client.crt -tls-client-key=/etc/deal/certs/deal-client.key -tls-server-name=localhost; else /bin/grpc_health_probe -addr=localhost:5082; fi"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 15s
|
||||
restart: unless-stopped
|
||||
|
||||
# telegram-service — gRPC-сервер Telegram (:5101; plaintext либо mTLS — как core). Сессии —
|
||||
# /data/sessions (volume deal_tg_sessions, AES-GCM ключом DEAL_TELEGRAM_SESSION_KEY, fail-closed).
|
||||
telegram-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/telegram-service/Deal.Telegram/Dockerfile
|
||||
<<: *service-hardening
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5101"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
|
||||
DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:?DEAL_TELEGRAM_SESSION_KEY не задан (ключ AES-GCM сессий)}
|
||||
DEAL_TELEGRAM_SESSION_DIR: /data/sessions
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082}
|
||||
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
|
||||
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
|
||||
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/telegram-service-server.pfx
|
||||
DEAL_MTLS_SERVER_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
DEAL_MTLS_CLIENT_CERT_PFX: /etc/deal/certs/deal-client.pfx
|
||||
DEAL_MTLS_CLIENT_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
volumes:
|
||||
- deal_tg_sessions:/data/sessions
|
||||
- ${DEAL_CERTS_DIR:-./certs}:/etc/deal/certs:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5101 -tls -tls-ca-cert=/etc/deal/certs/ca.pem -tls-client-cert=/etc/deal/certs/deal-client.crt -tls-client-key=/etc/deal/certs/deal-client.key -tls-server-name=localhost; else /bin/grpc_health_probe -addr=localhost:5101; fi"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
# ai-service — gRPC-сервер ИИ-фасада (:5102). Без БД и volume'ов (конфиг провайдера — в теле RPC).
|
||||
ai-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/ai-service/Deal.Ai/Dockerfile
|
||||
<<: *service-hardening
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5102"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
|
||||
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
|
||||
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/ai-service-server.pfx
|
||||
DEAL_MTLS_SERVER_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
DEAL_MTLS_CLIENT_CERT_PFX: /etc/deal/certs/deal-client.pfx
|
||||
DEAL_MTLS_CLIENT_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
volumes:
|
||||
- ${DEAL_CERTS_DIR:-./certs}:/etc/deal/certs:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5102 -tls -tls-ca-cert=/etc/deal/certs/ca.pem -tls-client-cert=/etc/deal/certs/deal-client.crt -tls-client-key=/etc/deal/certs/deal-client.key -tls-server-name=localhost; else /bin/grpc_health_probe -addr=localhost:5102; fi"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
# ml-service — gRPC-сервер ML (:5103). Модели — /data/ml (volume deal_ml_data, SQLite на тенанта).
|
||||
ml-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/ml-service/Deal.Ml/Dockerfile
|
||||
<<: *service-hardening
|
||||
mem_limit: 768m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5103"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
|
||||
DEAL_ML_DATA_DIR: /data/ml
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
|
||||
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
|
||||
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/ml-service-server.pfx
|
||||
DEAL_MTLS_SERVER_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
DEAL_MTLS_CLIENT_CERT_PFX: /etc/deal/certs/deal-client.pfx
|
||||
DEAL_MTLS_CLIENT_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
|
||||
volumes:
|
||||
- deal_ml_data:/data/ml
|
||||
- ${DEAL_CERTS_DIR:-./certs}:/etc/deal/certs:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5103 -tls -tls-ca-cert=/etc/deal/certs/ca.pem -tls-client-cert=/etc/deal/certs/deal-client.crt -tls-client-key=/etc/deal/certs/deal-client.key -tls-server-name=localhost; else /bin/grpc_health_probe -addr=localhost:5103; fi"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
# caddy — edge: наружу только :80/:443. TLS — плейсхолдер tls internal (см. Caddyfile: домен,
|
||||
# реальный сертификат/Cloudflare, CSP/HSTS). Статика — ../src/frontend/dist (СОБРАТЬ ДО up).
|
||||
caddy:
|
||||
image: caddy:2.9.1
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ../src/frontend/dist:/srv:ro
|
||||
# /data — локальный CA и сертификаты Caddy (переживают пересоздание контейнера).
|
||||
- deal_caddy_data:/data
|
||||
- deal_caddy_config:/config
|
||||
depends_on:
|
||||
core:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
# ── Observability (Ruling 7/9; метрики — этап 12, пакет A) — ПРОФИЛЬ: --profile observability ──
|
||||
# Логи: docker-логи (stdout контейнеров) → promtail → loki → grafana (127.0.0.1:3001).
|
||||
# Метрики: /metrics процессов (:9464) → prometheus (127.0.0.1:9090) → grafana (датасорс uid
|
||||
# `prometheus`). Доступ — оператору по SSH-туннелю; наружу grafana/prometheus НЕ публикуются.
|
||||
# Версии образов — фиксированные (при подъёме обновите до актуальных patch-релизов).
|
||||
|
||||
loki:
|
||||
image: grafana/loki:3.4.2
|
||||
profiles: ["observability"]
|
||||
command: -config.file=/etc/loki/loki.yml
|
||||
volumes:
|
||||
- ./observability/loki.yml:/etc/loki/loki.yml:ro
|
||||
- deal_loki_data:/loki
|
||||
restart: unless-stopped
|
||||
|
||||
# Prometheus — сбор метрик Deal-процессов (этап 12, пакет A): scrape /metrics каждого процесса
|
||||
# (:9464, HTTP/1.1) внутри compose-сети. Конфиг — deploy/observability/prometheus.yml (таргеты
|
||||
# core/telegram-service/ai-service/ml-service). UI — только loopback 127.0.0.1:9090 (оператору по
|
||||
# SSH-туннелю, как Grafana); наружу порт не публикуется. Retention 15 суток (volume deal_prometheus_data).
|
||||
prometheus:
|
||||
image: prom/prometheus:v3.5.0
|
||||
profiles: ["observability"]
|
||||
command:
|
||||
- --config.file=/etc/prometheus/prometheus.yml
|
||||
- --storage.tsdb.path=/prometheus
|
||||
- --storage.tsdb.retention.time=15d
|
||||
ports:
|
||||
# Только loopback хоста — наружу не публикуется (Ruling 9).
|
||||
- "127.0.0.1:9090:9090"
|
||||
volumes:
|
||||
- ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./observability/prometheus-rules.yml:/etc/prometheus/prometheus-rules.yml:ro
|
||||
- deal_prometheus_data:/prometheus
|
||||
restart: unless-stopped
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:3.4.2
|
||||
profiles: ["observability"]
|
||||
command: -config.file=/etc/promtail/promtail.yml
|
||||
volumes:
|
||||
- ./observability/promtail.yml:/etc/promtail/promtail.yml:ro
|
||||
# docker.sock — чтение docker-логов всех контейнеров (docker_sd). Rootless-докер: путь сокета
|
||||
# свой (напр. /run/user/<uid>/docker.sock) — поправьте монтирование и promtail.yml.
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- deal_promtail_data:/var/lib/promtail
|
||||
depends_on:
|
||||
loki:
|
||||
condition: service_started
|
||||
restart: unless-stopped
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:11.5.2
|
||||
profiles: ["observability"]
|
||||
environment:
|
||||
# Пароль задайте в .env.prod перед подъёмом профиля (пусто — grafana не получит admin-пароль).
|
||||
GF_SECURITY_ADMIN_USER: ${DEAL_GRAFANA_ADMIN_USER:-admin}
|
||||
GF_SECURITY_ADMIN_PASSWORD: ${DEAL_GRAFANA_ADMIN_PASSWORD:-}
|
||||
GF_USERS_ALLOW_SIGN_UP: "false"
|
||||
GF_AUTH_ANONYMOUS_ENABLED: "false"
|
||||
ports:
|
||||
# Только loopback хоста — оператору по SSH-туннелю (Ruling 9); наружу не публикуется.
|
||||
- "127.0.0.1:3001:3000"
|
||||
volumes:
|
||||
- ./observability/grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- ./observability/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
- deal_grafana_data:/var/lib/grafana
|
||||
depends_on:
|
||||
loki:
|
||||
condition: service_started
|
||||
prometheus:
|
||||
condition: service_started
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
deal_pgdata:
|
||||
deal_minio_data:
|
||||
deal_tg_sessions:
|
||||
deal_ml_data:
|
||||
deal_api_data:
|
||||
deal_caddy_data:
|
||||
deal_caddy_config:
|
||||
deal_loki_data:
|
||||
deal_promtail_data:
|
||||
deal_grafana_data:
|
||||
deal_prometheus_data:
|
||||
|
||||
@@ -1,388 +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": ""
|
||||
}
|
||||
{
|
||||
"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": ""
|
||||
}
|
||||
|
||||
@@ -1,410 +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": ""
|
||||
}
|
||||
{
|
||||
"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": ""
|
||||
}
|
||||
|
||||
@@ -1,228 +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": ""
|
||||
}
|
||||
{
|
||||
"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": ""
|
||||
}
|
||||
|
||||
@@ -1,303 +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": ""
|
||||
}
|
||||
{
|
||||
"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": ""
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,375 +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": ""
|
||||
}
|
||||
{
|
||||
"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": ""
|
||||
}
|
||||
|
||||
@@ -1,16 +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
|
||||
# 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
|
||||
|
||||
@@ -1,28 +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
|
||||
# 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
|
||||
|
||||
@@ -1,45 +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
|
||||
# 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
|
||||
|
||||
@@ -1,81 +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 }})."
|
||||
# Правила алертов 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 }})."
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
# Prometheus — сбор метрик Deal-процессов (этап 12, пакет A; Ruling 7 этапа 7).
|
||||
#
|
||||
# Источник — эндпоинты /metrics процессов Deal (OpenTelemetry → экспортёр Prometheus). Процессы
|
||||
# слушают метрики на ОТДЕЛЬНОМ HTTP/1.1 Kestrel-эндпоинте :9464 (gRPC-порты :5101–5103/: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
|
||||
# Prometheus — сбор метрик Deal-процессов (этап 12, пакет A; Ruling 7 этапа 7).
|
||||
#
|
||||
# Источник — эндпоинты /metrics процессов Deal (OpenTelemetry → экспортёр Prometheus). Процессы
|
||||
# слушают метрики на ОТДЕЛЬНОМ HTTP/1.1 Kestrel-эндпоинте :9464 (gRPC-порты :5101–5103/: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
|
||||
|
||||
@@ -1,52 +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:
|
||||
# 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:
|
||||
|
||||
Reference in New Issue
Block a user