Merge pull request 'Внедрить современный observability-стек (логи, метрики, трейсы, ресурсы)' (#18) from t17_observability into main
ci / build-test (push) Successful in 3m32s
ci / build-test (push) Successful in 3m32s
Reviewed-on: #18
This commit was merged in pull request #18.
This commit is contained in:
@@ -108,6 +108,9 @@ services:
|
||||
Storage__Minio__SecretKey: deal_minio_secret
|
||||
Storage__Minio__Bucket: deal-files
|
||||
Storage__Minio__Secure: "false"
|
||||
# Трейсинг OTel → коллектор профиля observability (без коллектора трейсы не экспортируются).
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||||
OTEL_SERVICE_NAME: core
|
||||
ports:
|
||||
- "5080:5080"
|
||||
- "5082:5082" # gRPC-ингресс telegram-service (сервисы ходят на http://core:5082 внутри сети)
|
||||
@@ -146,6 +149,8 @@ services:
|
||||
DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:-ZmVkY2JhOTg3NjU0MzIxMGZlZGNiYTk4NzY1NDMyMTA=}
|
||||
DEAL_TELEGRAM_SESSION_DIR: /data/sessions
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||||
OTEL_SERVICE_NAME: telegram-service
|
||||
SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082}
|
||||
ports:
|
||||
- "5101:5101"
|
||||
@@ -176,6 +181,8 @@ services:
|
||||
GRPC_PORT: "5102"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||||
OTEL_SERVICE_NAME: ai-service
|
||||
ports:
|
||||
- "5102:5102"
|
||||
healthcheck:
|
||||
@@ -203,6 +210,8 @@ services:
|
||||
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
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||||
OTEL_SERVICE_NAME: ml-service
|
||||
ports:
|
||||
- "5103:5103"
|
||||
volumes:
|
||||
@@ -233,6 +242,8 @@ services:
|
||||
DEAL_STORAGE_BUCKET: deal-attachments
|
||||
DEAL_STORAGE_SECURE: "false"
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||||
OTEL_SERVICE_NAME: storage-service
|
||||
ports:
|
||||
- "5104:5104"
|
||||
depends_on:
|
||||
@@ -261,6 +272,66 @@ services:
|
||||
- ./observability/prometheus-rules.yml:/etc/prometheus/prometheus-rules.yml:ro
|
||||
- deal_prometheus_data:/prometheus
|
||||
|
||||
# OpenTelemetry Collector — приём трейсов Deal-процессов (OTLP) → Tempo (профиль observability).
|
||||
otel-collector:
|
||||
image: otel/opentelemetry-collector-contrib:0.160.0
|
||||
container_name: deal-otel-collector
|
||||
profiles: ["observability"]
|
||||
command: ["--config=/etc/otelcol-contrib/config.yaml"]
|
||||
ports:
|
||||
- "4317:4317"
|
||||
- "4318:4318"
|
||||
volumes:
|
||||
- ./observability/otel-collector.yml:/etc/otelcol-contrib/config.yaml:ro
|
||||
depends_on:
|
||||
tempo:
|
||||
condition: service_started
|
||||
|
||||
# Tempo — хранилище трейсов (OTLP от коллектора), UI/API — :3200 (профиль observability).
|
||||
tempo:
|
||||
image: grafana/tempo:2.8.1
|
||||
container_name: deal-tempo
|
||||
profiles: ["observability"]
|
||||
command: ["-config.file=/etc/tempo.yml"]
|
||||
ports:
|
||||
- "3200:3200"
|
||||
volumes:
|
||||
- ./observability/tempo.yml:/etc/tempo.yml:ro
|
||||
- deal_tempo_data:/var/tempo
|
||||
|
||||
# cAdvisor — ресурсы контейнеров (CPU/RAM/сеть/диск); scrape — job cadvisor.
|
||||
cadvisor:
|
||||
image: gcr.io/cadvisor/cadvisor:v0.52.1
|
||||
container_name: deal-cadvisor
|
||||
profiles: ["observability"]
|
||||
privileged: true
|
||||
devices:
|
||||
- /dev/kmsg:/dev/kmsg
|
||||
volumes:
|
||||
- /:/rootfs:ro
|
||||
- /var/run:/var/run:ro
|
||||
- /sys:/sys:ro
|
||||
- /var/lib/docker/:/var/lib/docker:ro
|
||||
- /dev/disk/:/dev/disk:ro
|
||||
|
||||
# node-exporter — ресурсы хоста (CPU/RAM/диск/сеть); scrape — job node-exporter.
|
||||
node-exporter:
|
||||
image: prom/node-exporter:v1.9.1
|
||||
container_name: deal-node-exporter
|
||||
profiles: ["observability"]
|
||||
command:
|
||||
- --path.procfs=/host/proc
|
||||
- --path.sysfs=/host/sys
|
||||
- --path.rootfs=/host/root
|
||||
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($|/)
|
||||
ports:
|
||||
- "9100:9100"
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/host/root:ro
|
||||
pid: host
|
||||
|
||||
volumes:
|
||||
deal_pgdata:
|
||||
deal_minio_data:
|
||||
@@ -268,3 +339,4 @@ volumes:
|
||||
deal_ml_data:
|
||||
deal_api_data:
|
||||
deal_prometheus_data:
|
||||
deal_tempo_data:
|
||||
|
||||
+111
-5
@@ -6,12 +6,12 @@
|
||||
# Состав (всё в одной внутренней сети 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 (см. ниже);
|
||||
# ml-service (:5103), storage-service (:5104) — процессы «Дейла»; 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).
|
||||
# observability (ПРОФИЛЬ `observability`) — современный стек: otel-collector (приём трейсов OTLP),
|
||||
# tempo (хранилище трейсов), loki/promtail (логи), prometheus (метрики),
|
||||
# cadvisor/node-exporter (потребление ресурсов контейнеров/хоста), grafana (UI).
|
||||
# Подъём: docker compose --profile observability up -d.
|
||||
#
|
||||
# Секреты — ТОЛЬКО из env: шаблон deploy/.env.prod.example → скопируйте в deploy/.env.prod,
|
||||
# заполните значения и запускайте с --env-file:
|
||||
@@ -125,6 +125,7 @@ services:
|
||||
Services__Ai__Endpoint: ${DEAL_AI_ENDPOINT:-http://ai-service:5102}
|
||||
Services__Telegram__UseLocal: "false"
|
||||
Services__Telegram__Endpoint: ${DEAL_TELEGRAM_ENDPOINT:-http://telegram-service:5101}
|
||||
Services__Storage__Endpoint: ${DEAL_STORAGE_SERVICE_ENDPOINT:-http://storage-service:5104}
|
||||
# Файлы — MinIO (внутренний http; TLS minio — вне этапа, при желании Storage__Minio__Secure=true
|
||||
# + endpoint https и сертификаты).
|
||||
Storage__Minio__Endpoint: minio:9000
|
||||
@@ -132,6 +133,9 @@ services:
|
||||
Storage__Minio__SecretKey: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD не задан}
|
||||
Storage__Minio__Bucket: deal-files
|
||||
Storage__Minio__Secure: "false"
|
||||
# Трейсинг OTel → коллектор профиля observability; без коллектора трейсы не экспортируются.
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||||
OTEL_SERVICE_NAME: core
|
||||
# mTLS внутреннего gRPC (Ruling 6; пути — /etc/deal/certs, см. volume ниже).
|
||||
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
|
||||
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
|
||||
@@ -172,6 +176,8 @@ services:
|
||||
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
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||||
OTEL_SERVICE_NAME: telegram-service
|
||||
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
|
||||
@@ -201,6 +207,8 @@ services:
|
||||
GRPC_PORT: "5102"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||||
OTEL_SERVICE_NAME: ai-service
|
||||
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
|
||||
@@ -229,6 +237,8 @@ services:
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
|
||||
DEAL_ML_DATA_DIR: /data/ml
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||||
OTEL_SERVICE_NAME: ml-service
|
||||
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
|
||||
@@ -245,6 +255,44 @@ services:
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
# storage-service — общий gRPC-сервис данных (вложения источников), :5104. Бэкенд — MinIO.
|
||||
# Без host-портов; защита — общий service-token (+ mTLS при DEAL_MTLS_ENABLED=1).
|
||||
storage-service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: src/storage-service/Deal.Storage/Dockerfile
|
||||
<<: *service-hardening
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
environment:
|
||||
GRPC_PORT: "5104"
|
||||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
|
||||
DEAL_STORAGE_ENDPOINT: minio:9000
|
||||
DEAL_STORAGE_ACCESS_KEY: ${MINIO_ROOT_USER:?MINIO_ROOT_USER не задан}
|
||||
DEAL_STORAGE_SECRET_KEY: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD не задан}
|
||||
DEAL_STORAGE_BUCKET: deal-attachments
|
||||
DEAL_STORAGE_SECURE: "false"
|
||||
DEAL_LOGS_DIR: /tmp/logs
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||||
OTEL_SERVICE_NAME: storage-service
|
||||
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
|
||||
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
|
||||
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/storage-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
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5104 -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:5104; 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:
|
||||
@@ -334,6 +382,63 @@ services:
|
||||
condition: service_started
|
||||
prometheus:
|
||||
condition: service_started
|
||||
tempo:
|
||||
condition: service_started
|
||||
restart: unless-stopped
|
||||
|
||||
# ── Трейсы и ресурсы (observability-стек) ─────────────────────────────────
|
||||
# OpenTelemetry Collector — приёмник трейсов Deal-процессов (OTLP gRPC :4317 / HTTP :4318),
|
||||
# батчит и перекладывает в Tempo. Наружу порты не публикуются (внутри compose-сети).
|
||||
otel-collector:
|
||||
image: otel/opentelemetry-collector-contrib:0.160.0
|
||||
profiles: ["observability"]
|
||||
command: ["--config=/etc/otelcol-contrib/config.yaml"]
|
||||
volumes:
|
||||
- ./observability/otel-collector.yml:/etc/otelcol-contrib/config.yaml:ro
|
||||
depends_on:
|
||||
tempo:
|
||||
condition: service_started
|
||||
restart: unless-stopped
|
||||
|
||||
# Tempo — хранилище трейсов (OTLP от коллектора). Retention блоков — 7 суток (см. tempo.yml).
|
||||
tempo:
|
||||
image: grafana/tempo:2.8.1
|
||||
profiles: ["observability"]
|
||||
command: ["-config.file=/etc/tempo.yml"]
|
||||
volumes:
|
||||
- ./observability/tempo.yml:/etc/tempo.yml:ro
|
||||
- deal_tempo_data:/var/tempo
|
||||
restart: unless-stopped
|
||||
|
||||
# cAdvisor — ресурсы контейнеров (CPU/RAM/сеть/диск); scrape — job cadvisor в prometheus.yml.
|
||||
cadvisor:
|
||||
image: gcr.io/cadvisor/cadvisor:v0.52.1
|
||||
profiles: ["observability"]
|
||||
privileged: true
|
||||
devices:
|
||||
- /dev/kmsg:/dev/kmsg
|
||||
volumes:
|
||||
- /:/rootfs:ro
|
||||
- /var/run:/var/run:ro
|
||||
- /sys:/sys:ro
|
||||
- /var/lib/docker/:/var/lib/docker:ro
|
||||
- /dev/disk/:/dev/disk:ro
|
||||
restart: unless-stopped
|
||||
|
||||
# node-exporter — ресурсы хоста (CPU/RAM/диск/сеть); scrape — job node-exporter в prometheus.yml.
|
||||
node-exporter:
|
||||
image: prom/node-exporter:v1.9.1
|
||||
profiles: ["observability"]
|
||||
command:
|
||||
- --path.procfs=/host/proc
|
||||
- --path.sysfs=/host/sys
|
||||
- --path.rootfs=/host/root
|
||||
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($|/)
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/host/root:ro
|
||||
pid: host
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
@@ -343,6 +448,7 @@ volumes:
|
||||
deal_ml_data:
|
||||
deal_api_data:
|
||||
deal_caddy_data:
|
||||
deal_tempo_data:
|
||||
deal_caddy_config:
|
||||
deal_loki_data:
|
||||
deal_promtail_data:
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Observability-стек «Дейла»
|
||||
|
||||
Современный (vendor-neutral) стек мониторинга. Поднимается **профилем `observability`** (в prod —
|
||||
`deploy/compose.prod.yml`, в dev — `deploy/compose.dev.yml`); наружу порты не публикуются (prod —
|
||||
доступ оператору по SSH-туннелю).
|
||||
|
||||
## Состав и поток данных
|
||||
|
||||
| Слой | Сервис | Конфиг | Поток |
|
||||
|---|---|---|---|
|
||||
| Трейсы (приём) | `otel-collector` | `otel-collector.yml` | OTLP от сервисов (`:4317`) → Tempo |
|
||||
| Трейсы (хранение) | `tempo` | `tempo.yml` | OTLP от коллектора, retention 7 сут. |
|
||||
| Логи | `loki` + `promtail` | `loki.yml`, `promtail.yml` | docker-логи → Loki |
|
||||
| Метрики | `prometheus` | `prometheus.yml`, `prometheus-rules.yml` | scrape `/metrics` процессов и `cadvisor`/`node-exporter` |
|
||||
| Ресурсы контейнеров | `cadvisor` | — | Prometheus |
|
||||
| Ресурсы хоста | `node-exporter` | — | Prometheus |
|
||||
| Визуализация | `grafana` | `grafana/provisioning/**` | Loki + Prometheus + Tempo |
|
||||
|
||||
## Подъём
|
||||
|
||||
```bash
|
||||
# prod (нужен deploy/.env.prod с DEAL_GRAFANA_ADMIN_PASSWORD)
|
||||
docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml --profile observability up -d
|
||||
|
||||
# dev
|
||||
docker compose -f deploy/compose.dev.yml --profile observability up -d
|
||||
```
|
||||
|
||||
Проверка: Prometheus `/targets` (job `deal` — 4 процесса UP, `cadvisor`, `node-exporter`, `tempo`) →
|
||||
Grafana → папка «Дейл» → `Deal-Metrics-Overview` / `Deal-Logs` / `Deal-Traces` / `Deal-Resources`.
|
||||
|
||||
## Подключение сервисов (код)
|
||||
|
||||
Общая настройка — `Deal.Grpc.Hosting` (сервисы) и `Deal.Api/Observability` (ядро):
|
||||
|
||||
- **метрики**: `DealMetricsHosting` — OTel → Prometheus, отдельный HTTP/1.1-эндпоинт `/metrics:9464`
|
||||
(env `METRICS_PORT`);
|
||||
- **трейсы**: `DealTracingHosting` — OTel → OTLP, **опт-ин** через env `OTEL_EXPORTER_OTLP_ENDPOINT`
|
||||
(адрес коллектора; без него трейсинг выключен), имя сервиса — `OTEL_SERVICE_NAME`;
|
||||
- **логи**: Serilog JSON обогащается `TraceId`/`SpanId` (`TraceContextEnricher`) для связи с трейсами.
|
||||
|
||||
## Дашборды и алерты
|
||||
|
||||
Дашборды — как код: `grafana/dashboards/*.json` (правки только в репозитории, UI не сохраняет).
|
||||
Алерты — `prometheus-rules.yml` (доступность, ошибки/5xx, очереди). Алерты по **ресурсам** вынесены в
|
||||
`prometheus-resource-rules.yml` и **отключены по умолчанию**; включаются добавлением файла в `rule_files`,
|
||||
пороги — через env `DEAL_ALERT_*` (см. шапку файла).
|
||||
|
||||
## Обновление версий образов
|
||||
|
||||
Версии зафиксированы в compose-файлах. При обновлении — свежие стабильные теги:
|
||||
`otel/opentelemetry-collector-contrib`, `grafana/tempo`, `prom/node-exporter`,
|
||||
`gcr.io/cadvisor/cadvisor`, `grafana/loki`, `grafana/promtail`, `prom/prometheus`, `grafana/grafana`.
|
||||
@@ -0,0 +1,289 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"editable": true,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"description": "Потребление CPU контейнерами Deal (cAdvisor). Значение — ядра, занятые контейнером за 5 минут.",
|
||||
"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": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "sum by (name) (rate(container_cpu_usage_seconds_total{name=~\".*deal.*|.*core.*|.*service.*\"}[5m]))",
|
||||
"legendFormat": "{{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "CPU контейнеров (cAdvisor)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"description": "Рабочий набор памяти контейнеров Deal (cAdvisor). Лимиты заданы mem_limit в compose.prod.yml.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 15,
|
||||
"lineWidth": 1,
|
||||
"showPoints": "never"
|
||||
},
|
||||
"unit": "bytes"
|
||||
},
|
||||
"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": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "sum by (name) (container_memory_working_set_bytes{name=~\".*deal.*|.*core.*|.*service.*\"})",
|
||||
"legendFormat": "{{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Память контейнеров (cAdvisor)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"description": "Загрузка CPU хоста (node-exporter), 0–100%.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 15,
|
||||
"lineWidth": 1,
|
||||
"showPoints": "never"
|
||||
},
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "percent"
|
||||
},
|
||||
"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": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "100 - (avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
|
||||
"legendFormat": "CPU хоста",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "CPU хоста (node-exporter)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"description": "Занятая память хоста (node-exporter), проценты от общего объёма.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 15,
|
||||
"lineWidth": 1,
|
||||
"showPoints": "never"
|
||||
},
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "percent"
|
||||
},
|
||||
"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": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100",
|
||||
"legendFormat": "Память хоста",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Память хоста (node-exporter)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"description": "Свободное место на разделах хоста (node-exporter), проценты.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 15,
|
||||
"lineWidth": 1,
|
||||
"showPoints": "never"
|
||||
},
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "percent"
|
||||
},
|
||||
"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": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "min by (mountpoint) (node_filesystem_avail_bytes{fstype!~\"tmpfs|overlay\"} / node_filesystem_size_bytes{fstype!~\"tmpfs|overlay\"}) * 100",
|
||||
"legendFormat": "{{mountpoint}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Свободное место на диске (node-exporter)",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 39,
|
||||
"tags": [
|
||||
"deal",
|
||||
"resources"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timezone": "browser",
|
||||
"title": "Deal — Ресурсы",
|
||||
"uid": "deal-resources",
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"editable": true,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "tempo",
|
||||
"uid": "tempo"
|
||||
},
|
||||
"description": "Поиск трейсов Deal (Tempo, TraceQL). Источник — OTLP от сервисов через otel-collector. Клик по трейсу — спаны по сервисам; из спана можно перейти к логам (Loki) по traceId.",
|
||||
"gridPos": {
|
||||
"h": 22,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "tempo",
|
||||
"uid": "tempo"
|
||||
},
|
||||
"query": "{}",
|
||||
"queryType": "traceql",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Трейсы (Tempo)",
|
||||
"type": "traces"
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 39,
|
||||
"tags": [
|
||||
"deal",
|
||||
"traces"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timezone": "browser",
|
||||
"title": "Deal — Трейсы",
|
||||
"uid": "deal-traces",
|
||||
"version": 1
|
||||
}
|
||||
@@ -13,6 +13,12 @@ datasources:
|
||||
isDefault: true
|
||||
jsonData:
|
||||
maxLines: 1000
|
||||
# Клик по TraceId в логе открывает трейс в Tempo (обогащение логов TraceContextEnricher).
|
||||
derivedFields:
|
||||
- name: TraceID
|
||||
matcherRegex: '"TraceId":"([0-9a-f]+)"'
|
||||
datasourceUid: tempo
|
||||
url: "$${__value.raw}"
|
||||
|
||||
- name: Prometheus
|
||||
# UID фиксирован: на него ссылаются панели дашборда Deal-Metrics-Overview (datasource uid: prometheus).
|
||||
@@ -26,3 +32,24 @@ datasources:
|
||||
# Prometheus хранит OTel-гистограммы в нативных bucket'ах — используем нативные histogram_quantile.
|
||||
httpMethod: POST
|
||||
timeInterval: 15s
|
||||
|
||||
- name: Tempo
|
||||
# UID фиксирован: на него ссылаются панели Deal-Traces и derivedFields логов (datasource uid: tempo).
|
||||
uid: tempo
|
||||
type: tempo
|
||||
access: proxy
|
||||
url: http://tempo:3200
|
||||
isDefault: false
|
||||
jsonData:
|
||||
# Из спана трейса — к логам того же сервиса и traceId (обратная корреляция Loki ↔ Tempo).
|
||||
tracesToLogsV2:
|
||||
datasourceUid: loki
|
||||
filterByTraceID: true
|
||||
filterBySpanID: false
|
||||
spanStartTimeShift: -1m
|
||||
spanEndTimeShift: 1m
|
||||
# Карта сервисов и граф узлов — из метрик Prometheus.
|
||||
serviceMap:
|
||||
datasourceUid: prometheus
|
||||
nodeGraph:
|
||||
enabled: true
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# OpenTelemetry Collector — приёмник трейсов Deal-процессов (профиль observability).
|
||||
#
|
||||
# Процессы шлют OTLP (gRPC :4317 / HTTP :4318) на этот сервис; коллектор батчит и перекладывает
|
||||
# трейсы в Tempo (OTLP). Запускается только профилем observability (deploy/compose.prod.yml);
|
||||
# наружу порты не публикуются (внутри compose-сети).
|
||||
#
|
||||
# Конфиг монтируется в /etc/otelcol-contrib/config.yaml. Health-эндпоинт :13133 — для healthcheck.
|
||||
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
|
||||
processors:
|
||||
# Батчинг снижает число сетевых вызовов к Tempo (стандартный приём OTel).
|
||||
batch: {}
|
||||
|
||||
exporters:
|
||||
otlp/tempo:
|
||||
endpoint: tempo:4317
|
||||
tls:
|
||||
insecure: true
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
|
||||
service:
|
||||
extensions: [health_check]
|
||||
telemetry:
|
||||
logs:
|
||||
level: warn
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [otlp/tempo]
|
||||
@@ -0,0 +1,73 @@
|
||||
# Правила алертов Prometheus по потреблению ресурсов — ОТКЛЮЧЕНЫ ПО УМОЛЧАНИЮ.
|
||||
#
|
||||
# Заготовка: включить, добавив эту строку в `rule_files` файла prometheus.yml:
|
||||
# rule_files:
|
||||
# - prometheus-rules.yml
|
||||
# - prometheus-resource-rules.yml
|
||||
#
|
||||
# Пороги выносятся в env (DEAL_ALERT_*); Prometheus не раскрывает значения env в конфиге, поэтому при
|
||||
# включении файл рендерится из шаблона (подстановка порогов) или пороги проставляются вручную:
|
||||
# DEAL_ALERT_HOST_CPU_PERCENT — загрузка CPU хоста, % (дефолт 90)
|
||||
# DEAL_ALERT_HOST_MEMORY_PERCENT — занятая память хоста, % (дефолт 90)
|
||||
# DEAL_ALERT_HOST_DISK_PERCENT — минимум свободного места, % (дефолт 15)
|
||||
# DEAL_ALERT_CONTAINER_CPU — CPU контейнера (ядра) (дефолт 1.5)
|
||||
# DEAL_ALERT_CONTAINER_MEMORY_PERCENT — память контейнера к лимиту, %(дефолт 90)
|
||||
#
|
||||
# Источники: node-exporter (хост) и cAdvisor (контейнеры), scrape — jobs node-exporter/cadvisor.
|
||||
|
||||
groups:
|
||||
- name: deal-resources
|
||||
rules:
|
||||
# Хост: загрузка CPU. Дефолт порога — 90%.
|
||||
- alert: DealHostHighCpu
|
||||
expr: 100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Высокая загрузка CPU хоста"
|
||||
description: "Средняя загрузка CPU хоста выше 90% за 5 минут более 10 минут."
|
||||
|
||||
# Хост: занятая память. Дефолт порога — 90%.
|
||||
- alert: DealHostHighMemory
|
||||
expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.9
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Высокое потребление памяти хостом"
|
||||
description: "Свободной памяти меньше 10% более 10 минут."
|
||||
|
||||
# Хост: свободное место на разделе. Дефолт порога — 15%.
|
||||
- alert: DealHostDiskLow
|
||||
expr: |
|
||||
min by (mountpoint) (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}) < 0.15
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Мало места на диске ({{ $labels.mountpoint }})"
|
||||
description: "Свободно менее 15% на разделе {{ $labels.mountpoint }} более 15 минут."
|
||||
|
||||
# Контейнеры Deal: CPU (ядра). Дефолт порога — 1.5.
|
||||
- alert: DealContainerHighCpu
|
||||
expr: |
|
||||
sum by (name) (rate(container_cpu_usage_seconds_total{name=~".*deal.*|.*core.*|.*service.*"}[5m])) > 1.5
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Высокий CPU контейнера {{ $labels.name }}"
|
||||
description: "Контейнер {{ $labels.name }} держит > 1.5 CPU за 5 минут более 10 минут."
|
||||
|
||||
# Контейнеры Deal: память к лимиту. Дефолт порога — 90%.
|
||||
- alert: DealContainerHighMemory
|
||||
expr: |
|
||||
(container_memory_working_set_bytes{name=~".*deal.*|.*core.*|.*service.*"}
|
||||
/ clamp_min(container_spec_memory_limit_bytes{name=~".*deal.*|.*core.*|.*service.*"}, 1)) > 0.9
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Контейнер {{ $labels.name }} близок к лимиту памяти"
|
||||
description: "Контейнер {{ $labels.name }} использует более 90% лимита памяти более 10 минут."
|
||||
@@ -79,3 +79,4 @@ groups:
|
||||
annotations:
|
||||
summary: "Растёт очередь обучения ML (outbox)"
|
||||
description: "Суммарная глубина MlOutbox держится выше 100 более 15 минут ({{ $value }})."
|
||||
|
||||
|
||||
@@ -46,3 +46,18 @@ scrape_configs:
|
||||
- targets: ["ml-service:9464"]
|
||||
labels:
|
||||
service: ml-service
|
||||
|
||||
# Ресурсы контейнеров (cAdvisor) и хоста (node-exporter) — этап «observability-стек».
|
||||
# cAdvisor отдаёт метрики контейнеров (CPU/RAM/сеть/диск), node-exporter — хоста.
|
||||
- job_name: cadvisor
|
||||
static_configs:
|
||||
- targets: ["cadvisor:8080"]
|
||||
|
||||
- job_name: node-exporter
|
||||
static_configs:
|
||||
- targets: ["node-exporter:9100"]
|
||||
|
||||
# Темпо (хранилище трейсов) — само-мониторинг: метрики приёма/отдачи трейсов.
|
||||
- job_name: tempo
|
||||
static_configs:
|
||||
- targets: ["tempo:3200"]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Grafana Tempo — хранилище трейсов Deal (профиль observability).
|
||||
#
|
||||
# Принимает OTLP-трейсы от OpenTelemetry Collector (grpc :4317), хранит локально (volume
|
||||
# deal_tempo_data), отдаёт запросы Grafana на :3200 (внутри compose-сети). Block retention — 7 суток
|
||||
# (трейсы объёмны; логи/метрики живут дольше).
|
||||
|
||||
server:
|
||||
http_listen_port: 3200
|
||||
log_level: warn
|
||||
|
||||
distributor:
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
|
||||
storage:
|
||||
trace:
|
||||
backend: local
|
||||
local:
|
||||
path: /var/tempo/traces
|
||||
wal:
|
||||
path: /var/tempo/wal
|
||||
|
||||
compactor:
|
||||
compaction:
|
||||
block_retention: 168h
|
||||
@@ -22,7 +22,7 @@
|
||||
| Файлы | MinIO (S3-совместимое хранилище) |
|
||||
| Фронтенд | Vue 3 + Vite + Tailwind |
|
||||
| Межсервисно | gRPC + Protobuf (mTLS — за флагом `DEAL_MTLS_*`, §10/§13.8) |
|
||||
| Наблюдаемость | Serilog (JSON: консоль + rolling-файл) → Promtail → Loki → Grafana; метрики OTel → Prometheus → Grafana |
|
||||
| Наблюдаемость | Serilog (JSON: консоль + rolling-файл) → Promtail → Loki → Grafana; метрики OTel → Prometheus; трейсы OTel → Collector → Tempo; ресурсы cAdvisor/node-exporter → Prometheus; единый UI — Grafana |
|
||||
| Прокси/edge | Caddy (TLS, security-заголовки); Cloudflare/k8s — вне этапа (§10/§11) |
|
||||
| Контейнеры | Docker / docker compose (VPS); k8s — позже |
|
||||
| Бэкапы | Ежедневные: pg_dump + MinIO + сессии |
|
||||
@@ -236,7 +236,9 @@ settings(Key varchar(200) PK, ValueJson text, UpdatedAt timestamptz) --
|
||||
(нужен `DEAL_GRAFANA_ADMIN_PASSWORD` в `.env.prod`; порты Grafana/Prometheus — только loopback). Остановка —
|
||||
`docker compose -f deploy/compose.prod.yml --profile observability down`.
|
||||
- **Провижининг Grafana — как код** (`deploy/observability/grafana/provisioning`, монтируется в
|
||||
контейнер): `datasources/datasources.yml` — датасорс Loki (uid `loki`, URL `http://loki:3100`);
|
||||
контейнер): `datasources/datasources.yml` — датасорсы Loki (uid `loki`, default), Prometheus
|
||||
(uid `prometheus`) и Tempo (uid `tempo`); у Loki — `derivedFields` TraceID → Tempo (клик по traceId
|
||||
в логе открывает трейс), у Tempo — `tracesToLogsV2` → Loki и `serviceMap`/`nodeGraph` по метрикам;
|
||||
`dashboards/dashboards.yml` — папка `Дейл` из `/var/lib/grafana/dashboards`. Дашборды — файлы
|
||||
`deploy/observability/grafana/dashboards/*.json`: правки только в репозитории, UI-изменения не
|
||||
сохраняются (`allowUiUpdates: false`).
|
||||
@@ -251,7 +253,10 @@ settings(Key varchar(200) PK, ValueJson text, UpdatedAt timestamptz) --
|
||||
access-лога core) и активация инвайтов (`/api/join`);
|
||||
- `Deal-Errors` — HTTP 5xx, необработанные исключения (`@x`), Error/Fatal, ошибки gRPC и общая лента;
|
||||
- `Deal-Rps` — нагрузка HTTP+gRPC (RPS), top-путей/методов и p50/p95 длительности запроса;
|
||||
- `Deal-Logs` — обзор логов с фильтрами по сервису и уровню, активность по тенантам (AI/ML/Telegram).
|
||||
- `Deal-Logs` — обзор логов с фильтрами по сервису и уровню, активность по тенантам (AI/ML/Telegram);
|
||||
- `Deal-Traces` — поиск трейсов (Tempo, TraceQL), спаны по сервисам, переход к логам по traceId;
|
||||
- `Deal-Resources` — потребление ресурсов контейнерами (cAdvisor) и хостом (node-exporter): CPU/RAM,
|
||||
свободное место на дисках.
|
||||
**Актор в логах:** с BL-LOG-ACTOR access-лог включает `actor` (login пользователя/оператора) и
|
||||
`tenant`; полная лента действий с деталями — `public.audit_log` (append-only) через
|
||||
`GET /api/operator/audit` / экран «Аудит» оператор-консоли.
|
||||
@@ -295,9 +300,37 @@ settings(Key varchar(200) PK, ValueJson text, UpdatedAt timestamptz) --
|
||||
`deal_ml_outbox_depth`), пропажа метрик ядра (`absent(deal_sessions_active)`). Замечание: правила
|
||||
бюджета токенов нет — метрика бюджета в Prometheus отсутствует (см. §6/§10), поэтому алерт не вводится.
|
||||
- Как поднять/проверить: `docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml
|
||||
--profile observability up -d` → Prometheus `/targets` (все 4 UP) → Grafana → папка «Дейл» →
|
||||
`Deal-Metrics-Overview`. Быстрая проверка экспортёра без Grafana: `curl http://<процесс>:9464/metrics`
|
||||
изнутри сети.
|
||||
--profile observability up -d` → Prometheus `/targets` (все UP) → Grafana → папка «Дейл» →
|
||||
`Deal-Metrics-Overview`/`Deal-Resources`/`Deal-Traces`. Быстрая проверка экспортёра без Grafana:
|
||||
`curl http://<процесс>:9464/metrics` изнутри сети.
|
||||
|
||||
### Трейсы (OpenTelemetry Collector + Tempo)
|
||||
|
||||
- **Экспорт из 5 процессов**: OpenTelemetry SDK → OTLP → **OpenTelemetry Collector** (`otel-collector:
|
||||
4317`) → **Tempo** (`tempo:4317`, хранилище трейсов, retention 7 суток). Настройка — общая в
|
||||
`Deal.Grpc.Hosting` (`DealTracingHosting`) для telegram/ai/ml/storage и `Deal.Api/Observability/
|
||||
DealTracingHosting.cs` для ядра. Инструментируется входящий HTTP/gRPC (AspNetCore), исходящие
|
||||
HTTP-клиенты и gRPC-клиенты (GrpcNetClient) — трейсы сквозные от входа до БД/внешних сервисов.
|
||||
- **Включение — опт-ин через env** `OTEL_EXPORTER_OTLP_ENDPOINT` (адрес коллектора, напр.
|
||||
`http://otel-collector:4317`); без него трейсинг выключен. В compose env задан пустым
|
||||
(`${DEAL_OTEL_ENDPOINT:-}`) — чтобы включить, задайте `DEAL_OTEL_ENDPOINT` в `.env`. Имя сервиса в
|
||||
трейсах — `OTEL_SERVICE_NAME` (дефолт по процессу: `core`, `telegram-service`, `ai-service`,
|
||||
`ml-service`, `storage-service`).
|
||||
- **Корреляция с логами**: Serilog обогащается `TraceId`/`SpanId` из `Activity.Current`
|
||||
(`TraceContextEnricher`) — в Loki-логе есть `TraceId`, а датасорс Loki `derivedFields` даёт переход
|
||||
из лога в трейс Tempo (и обратно — `tracesToLogsV2`).
|
||||
- Сервисы профиля: `otel-collector` (`otel/opentelemetry-collector-contrib:0.160.0`, конфиг
|
||||
`deploy/observability/otel-collector.yml`) и `tempo` (`grafana/tempo:2.8.1`, конфиг
|
||||
`deploy/observability/tempo.yml`, volume `deal_tempo_data`). Наружу порты не публикуются (dev — для отладки).
|
||||
|
||||
### Ресурсы (cAdvisor + node-exporter)
|
||||
|
||||
- **cAdvisor** (`gcr.io/cadvisor/cadvisor:v0.52.1`) — потребление ресурсов **контейнерами**
|
||||
(CPU/RAM/сеть/диск); **node-exporter** (`prom/node-exporter:v1.9.1`) — ресурсы **хоста** (CPU/RAM/
|
||||
диски/сеть). Оба scrape'ит Prometheus (jobs `cadvisor`, `node-exporter` в `prometheus.yml`).
|
||||
- Дашборд `Deal-Resources` (uid `deal-resources`): CPU/RAM контейнеров, CPU/RAM хоста, свободное место
|
||||
на дисках. Правила алертов по ресурсам — в отдельном файле `prometheus-resource-rules.yml`,
|
||||
**отключены по умолчанию** (не входят в `rule_files`); пороги — через env `DEAL_ALERT_*` при включении.
|
||||
|
||||
---
|
||||
|
||||
@@ -330,10 +363,11 @@ settings(Key varchar(200) PK, ValueJson text, UpdatedAt timestamptz) --
|
||||
- Одна внутренняя сеть; наружу — только **caddy** (80/443): TLS (шапка `deploy/caddy/Caddyfile` —
|
||||
`tls internal` для dev/интранет, для реального домена заменить на Cloudflare-origin/сертификаты),
|
||||
статика `src/frontend/dist`, `reverse_proxy /api → core:5080`, security-заголовки (CSP/HSTS — здесь).
|
||||
- `core` (:5080 http + :5082 gRPC-ингресс), `telegram/ai/ml-service` (mTLS-env, Ruling 6),
|
||||
- `core` (:5080 http + :5082 gRPC-ингресс), `telegram/ai/ml/storage-service` (mTLS-env, Ruling 6),
|
||||
`postgres`/`minio` **без host-портов**; healthcheck'и — `grpc_health_probe` (при mTLS — TLS-проба с
|
||||
PEM `deal-client.crt/.key`)/`pg_isready`.
|
||||
- Профиль `observability`: `loki`/`promtail`/`grafana` + `prometheus` (метрики — этап 12, пакет A; см. §7).
|
||||
- Профиль `observability`: `otel-collector`/`tempo` (трейсы), `loki`/`promtail` (логи),
|
||||
`prometheus`/`cadvisor`/`node-exporter` (метрики и ресурсы), `grafana` (UI); см. §7.
|
||||
Секреты — только из `.env.prod`
|
||||
(шаблон `deploy/.env.prod.example`, без дефолтных паролей; отсутствие → fail-fast `:?`).
|
||||
Rate limiting включён (`RateLimit__Enabled: true`), CORS — явный `Security__AllowedOrigins`
|
||||
@@ -1152,11 +1186,12 @@ docker compose -f deploy/compose.dev.yml down # погасить ст
|
||||
(RpcCallLoggingInterceptor; gRPC-health не логируется). **Метрики** — OTel → Prometheus: `/metrics`
|
||||
(HTTP/1.1 :9464) + прикладные `deal.*` (токены/вызовы AI/ML, аудит, глубины очередей, сессии) — см. §7.
|
||||
PROD-стек: docker-логи → Promtail → Loki (retention 7 сут.) → Grafana (`127.0.0.1:3001`, SSH-туннель),
|
||||
метрики → Prometheus (`127.0.0.1:9090`) → Grafana; профиль `observability` compose.prod.
|
||||
метрики → Prometheus (`127.0.0.1:9090`) → Grafana, трейсы OTel → otel-collector → Tempo, ресурсы
|
||||
cAdvisor/node-exporter → Prometheus; профиль `observability` compose.prod.
|
||||
- **compose.prod** (Ruling 9): `deploy/compose.prod.yml` — postgres/minio (без host-портов), core + telegram/ai/ml
|
||||
(mTLS env; healthcheck — `grpc_health_probe`, при mTLS — TLS-проба с PEM), `caddy` (80/443: статика
|
||||
`src/frontend/dist` + `reverse_proxy /api → core:5080`, security-заголовки; домен/TLS/Cloudflare — шапка
|
||||
`deploy/caddy/Caddyfile`), профиль `observability` (loki/promtail/grafana/prometheus). Секреты — только из `.env.prod`
|
||||
`deploy/caddy/Caddyfile`), профиль `observability` (otel-collector/tempo/loki/promtail/prometheus/cadvisor/node-exporter/grafana). Секреты — только из `.env.prod`
|
||||
(шаблон `deploy/.env.prod.example`, без дефолтных паролей, fail-fast `:?`). Запуск:
|
||||
`docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml up -d --build` (+ `--profile observability`);
|
||||
авто-проверка — `... config` rc=0.
|
||||
@@ -1305,7 +1340,8 @@ docker compose -f deploy/compose.dev.yml start core telegram-service ml-service
|
||||
(`container_created`, `container_updated`, `container_deleted`), `settings_updated`, `channel_enabled`,
|
||||
`telegram_linked` (таблица — в контракте; `channel_created` зарезервирован, но не эмитится).
|
||||
- **Наблюдаемость** (Grafana provisioning + promtail-лейблы, дашборды `Deal-Auth/Errors/Rps/Logs`; с этапа 12 —
|
||||
метрики OTel → Prometheus и дашборд `Deal-Metrics-Overview`, см. §7).
|
||||
метрики OTel → Prometheus и дашборд `Deal-Metrics-Overview`; трейсы OTel → Collector → Tempo (`Deal-Traces`)
|
||||
и ресурсы cAdvisor/node-exporter (`Deal-Resources`), см. §7).
|
||||
- **Как открыть (dev):** `docker compose -f deploy/compose.dev.yml up -d --build` (или core на `:5080`
|
||||
с Postgres `:5433`, AI в Local-режиме) → фронт `cd src/frontend && npm run dev` (`:5173`, прокси `/api`)
|
||||
→ **оператор:** `http://localhost:5173/#/operator`, вход `operator`/`operator` (dev-дефолт; в Production —
|
||||
|
||||
@@ -18,6 +18,7 @@ WebApplication app = AiServiceHost.Create(
|
||||
{
|
||||
DealLogging.Configure(builder, aiProcessName);
|
||||
DealMetricsHosting.AddDealMetrics(builder, metricsPort);
|
||||
DealTracingHosting.AddDealTracing(builder, aiProcessName);
|
||||
});
|
||||
|
||||
DealMetricsHosting.MapDealMetrics(app);
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
Версии — 1.17.x (Prometheus-экспортёр и gRPC-клиент только pre-release-линией; остальные —
|
||||
1.17.0 stable). Настройка — Deal.Api/Observability/DealMetricsHosting.cs. -->
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.17.0-beta.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
|
||||
|
||||
@@ -62,7 +62,8 @@ internal static class DealLogging
|
||||
.MinimumLevel.Is(ParseMinimumLevel(configuration[MinimumLevelEnvKey]))
|
||||
.MinimumLevel.Override(EntityFrameworkCoreCategory, LogEventLevel.Warning)
|
||||
.MinimumLevel.Override(GrpcCategory, LogEventLevel.Information)
|
||||
.Enrich.FromLogContext();
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.With<TraceContextEnricher>();
|
||||
|
||||
string logsDirectory = ResolveLogsDirectory(environment.ContentRootPath, configuration[LogsDirectoryEnvKey]);
|
||||
Directory.CreateDirectory(logsDirectory);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Deal.Api.Logging;
|
||||
|
||||
internal sealed class TraceContextEnricher : ILogEventEnricher
|
||||
{
|
||||
void ILogEventEnricher.Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
|
||||
{
|
||||
Activity? activity = Activity.Current;
|
||||
if (activity is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TraceId", activity.TraceId.ToString()));
|
||||
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("SpanId", activity.SpanId.ToString()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace Deal.Api.Observability;
|
||||
|
||||
/// <summary>
|
||||
/// Настройка трейсинга ядра Deal.Api
|
||||
/// </summary>
|
||||
public static class DealTracingHosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Env-ключ OTLP-endpoint коллектора
|
||||
/// </summary>
|
||||
public const string OtlpEndpointEnvKey = "OTEL_EXPORTER_OTLP_ENDPOINT";
|
||||
|
||||
/// <summary>
|
||||
/// Env-ключ имени сервиса в трейсах
|
||||
/// </summary>
|
||||
public const string ServiceNameEnvKey = "OTEL_SERVICE_NAME";
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует трейсинг OpenTelemetry с экспортом OTLP
|
||||
/// </summary>
|
||||
/// <param name="builder">Билдер ядра.</param>
|
||||
/// <param name="defaultServiceName">Имя сервиса, если env OTEL_SERVICE_NAME не задан.</param>
|
||||
public static void AddDealTracing(WebApplicationBuilder builder, string defaultServiceName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(defaultServiceName);
|
||||
|
||||
// Трейсинг выключен без OTLP-endpoint (dev без профиля observability): иначе экспортёр
|
||||
// вхолостую спамит ошибками соединения.
|
||||
string? endpoint = Environment.GetEnvironmentVariable(OtlpEndpointEnvKey);
|
||||
if (string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string serviceName = Environment.GetEnvironmentVariable(ServiceNameEnvKey) is { Length: > 0 } configured
|
||||
? configured
|
||||
: defaultServiceName;
|
||||
|
||||
builder.Services
|
||||
.AddOpenTelemetry()
|
||||
.WithTracing(tracing => tracing
|
||||
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName))
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddGrpcClientInstrumentation()
|
||||
.AddOtlpExporter(options => options.Endpoint = new Uri(endpoint)));
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,7 @@ DealLogging.Configure(builder, coreProcessName);
|
||||
|
||||
int metricsPort = DealMetricsHosting.ResolveMetricsPort(DealMetricsHosting.DefaultMetricsPort);
|
||||
DealMetricsHosting.AddDealMetrics(builder, metricsPort);
|
||||
DealTracingHosting.AddDealTracing(builder, coreProcessName);
|
||||
|
||||
// Строка подключения Postgres — ТОЛЬКО из конфигурации (env/appsettings): dev-пароль в коде отсутствует
|
||||
// (Security review). Отсутствие строки = fail-fast на старте, а не тихий уход на несуществующую dev-БД.
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
ней нет; держим для будущего трейсинга). Версии — 1.17.x (Prometheus-экспортёр и gRPC-клиент
|
||||
выпускаются только pre-release-линией; остальные пакеты 1.17.0 stable). -->
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.17.0-beta.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
|
||||
|
||||
@@ -67,7 +67,8 @@ public static class DealLogging
|
||||
loggerConfiguration
|
||||
.MinimumLevel.Is(ParseMinimumLevel(configuration[MinimumLevelEnvKey]))
|
||||
.MinimumLevel.Override(GrpcCategory, LogEventLevel.Information)
|
||||
.Enrich.FromLogContext();
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.With<TraceContextEnricher>();
|
||||
|
||||
string logsDirectory = ResolveLogsDirectory(environment.ContentRootPath, configuration[LogsDirectoryEnvKey]);
|
||||
Directory.CreateDirectory(logsDirectory);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace Deal.Grpc.Hosting.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Общая настройка трейсинга Deal-сервисов
|
||||
/// </summary>
|
||||
public static class DealTracingHosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Env-ключ OTLP-endpoint коллектора
|
||||
/// </summary>
|
||||
public const string OtlpEndpointEnvKey = "OTEL_EXPORTER_OTLP_ENDPOINT";
|
||||
|
||||
/// <summary>
|
||||
/// Env-ключ имени сервиса в трейсах
|
||||
/// </summary>
|
||||
public const string ServiceNameEnvKey = "OTEL_SERVICE_NAME";
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует трейсинг OpenTelemetry с экспортом OTLP
|
||||
/// </summary>
|
||||
/// <param name="builder">Билдер хоста сервиса.</param>
|
||||
/// <param name="defaultServiceName">Имя сервиса, если env OTEL_SERVICE_NAME не задан.</param>
|
||||
public static void AddDealTracing(WebApplicationBuilder builder, string defaultServiceName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(defaultServiceName);
|
||||
|
||||
// Трейсинг выключен без OTLP-endpoint (dev без профиля observability): иначе экспортёр
|
||||
// вхолостую спамит ошибками соединения.
|
||||
string? endpoint = Environment.GetEnvironmentVariable(OtlpEndpointEnvKey);
|
||||
if (string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string serviceName = Environment.GetEnvironmentVariable(ServiceNameEnvKey) is { Length: > 0 } configured
|
||||
? configured
|
||||
: defaultServiceName;
|
||||
|
||||
builder.Services
|
||||
.AddOpenTelemetry()
|
||||
.WithTracing(tracing => tracing
|
||||
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName))
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddGrpcClientInstrumentation()
|
||||
.AddOtlpExporter(options => options.Endpoint = new Uri(endpoint)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Deal.Grpc.Hosting.Services;
|
||||
|
||||
internal sealed class TraceContextEnricher : ILogEventEnricher
|
||||
{
|
||||
void ILogEventEnricher.Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
|
||||
{
|
||||
Activity? activity = Activity.Current;
|
||||
if (activity is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TraceId", activity.TraceId.ToString()));
|
||||
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("SpanId", activity.SpanId.ToString()));
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ WebApplication app = MlServiceHost.Create(
|
||||
{
|
||||
DealLogging.Configure(builder, mlProcessName);
|
||||
DealMetricsHosting.AddDealMetrics(builder, metricsPort);
|
||||
DealTracingHosting.AddDealTracing(builder, mlProcessName);
|
||||
});
|
||||
|
||||
DealMetricsHosting.MapDealMetrics(app);
|
||||
|
||||
@@ -16,6 +16,7 @@ WebApplication app = StorageServiceHost.Create(
|
||||
{
|
||||
DealLogging.Configure(builder, storageProcessName);
|
||||
DealMetricsHosting.AddDealMetrics(builder, metricsPort);
|
||||
DealTracingHosting.AddDealTracing(builder, storageProcessName);
|
||||
});
|
||||
|
||||
DealMetricsHosting.MapDealMetrics(app);
|
||||
|
||||
@@ -18,6 +18,7 @@ WebApplication app = TelegramServiceHost.Create(
|
||||
{
|
||||
DealLogging.Configure(builder, telegramProcessName);
|
||||
DealMetricsHosting.AddDealMetrics(builder, metricsPort);
|
||||
DealTracingHosting.AddDealTracing(builder, telegramProcessName);
|
||||
});
|
||||
|
||||
DealMetricsHosting.MapDealMetrics(app);
|
||||
|
||||
Reference in New Issue
Block a user