Единый observability-стек: Prometheus + Loki + Grafana + Caddy auth-гейт
Отдельный от db+vaultwarden compose-файл (разный жизненный цикл, свой deploy-workflow) — единственное место сбора для backend, лендинга и каждой прокси-ноды, которые физически живут на других серверах. Prometheus скрейпит наружу (сам ходит в /metrics на тех хостах — там нужно только открыть порт метрик для IP этого VPS). Loki, наоборот, принимает пуши логов ИЗВНЕ — порт 3100 публикуется наружу и обязательно фильтруется firewall'ом по IP тех хостов. Grafana не торчит в интернет напрямую (127.0.0.1-only) — единственный вход через Caddy с forward_auth, гейтящим по cookie-сессии netrunner-backend (GET /api/v1/admin/observability/check, Owner/Moderator). 3 provisioned дашборда (backend/landing/прокси-ноды). Список scrape-таргетов для прокси-нод (observability/targets/nodes.json) генерится и заливается автоматически из netrunner-proxy при каждом деплое нод — вручную ничего не поддерживается. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,3 +41,12 @@ SMTP_PASSWORD=CHANGE_ME_GMAIL_APP_PASSWORD
|
|||||||
# BACKUP_RETENTION_DAYS=14
|
# BACKUP_RETENTION_DAYS=14
|
||||||
# RCLONE_REMOTE=b2:netrunner-backups
|
# RCLONE_REMOTE=b2:netrunner-backups
|
||||||
# RCLONE_CONFIG_CONTENT содержимое rclone.conf одной переменной, см. rclone.conf.example
|
# RCLONE_CONFIG_CONTENT содержимое rclone.conf одной переменной, см. rclone.conf.example
|
||||||
|
|
||||||
|
# --- Наблюдаемость (docker-compose.observability.yml — Prometheus/Loki/
|
||||||
|
# Grafana, отдельный стек от Postgres/Vaultwarden выше) ---
|
||||||
|
GRAFANA_PASSWORD=CHANGE_ME_GRAFANA_PASSWORD
|
||||||
|
# ВАЖНО: порт Loki (3100) публикуется наружу — сюда пушат логи тонкие
|
||||||
|
# promtail с backend/landing/каждой прокси-ноды. Firewall на этом VPS
|
||||||
|
# ОБЯЗАН ограничивать 3100 только IP этих хостов (см. шапку
|
||||||
|
# docker-compose.observability.yml) — сам Loki не защита от постороннего
|
||||||
|
# пуша логов.
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: Deploy Observability
|
||||||
|
|
||||||
|
# Отдельный от deploy.yml workflow — Prometheus/Loki/Grafana не должны
|
||||||
|
# рестартовать вместе с Postgres/Vaultwarden (docker-compose.observability.yml
|
||||||
|
# вместо docker-compose.yml, разные жизненные циклы). Деплоится на тот же
|
||||||
|
# DATA_VPS_IP/DATA_SSH_PRIVATE_KEY, что и основной стек — тот же VPS.
|
||||||
|
#
|
||||||
|
# GRAFANA_PASSWORD — единственный настоящий секрет здесь, остальное (список
|
||||||
|
# таргетов, конфиги Loki/promtail/Grafana-datasources) — публичный код
|
||||||
|
# репозитория, не секреты.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy Observability Stack
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Sync compose file + observability configs to VPS
|
||||||
|
uses: appleboy/scp-action@v0.1.7
|
||||||
|
with:
|
||||||
|
host: ${{ vars.DATA_VPS_IP }}
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.DATA_SSH_PRIVATE_KEY }}
|
||||||
|
source: "docker-compose.observability.yml,observability"
|
||||||
|
target: "/root/netrunner-data"
|
||||||
|
strip_components: 0
|
||||||
|
|
||||||
|
- name: Write .env and deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ vars.DATA_VPS_IP }}
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.DATA_SSH_PRIVATE_KEY }}
|
||||||
|
envs: "GRAFANA_PASSWORD"
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
cd /root/netrunner-data
|
||||||
|
|
||||||
|
# Свой .env.observability, НЕ общий .env (тот принадлежит
|
||||||
|
# deploy.yml/docker-compose.yml и перезаписывается им целиком на
|
||||||
|
# каждом своём прогоне — общий файл рассинхронизировал бы стеки).
|
||||||
|
cat > .env.observability <<EOF
|
||||||
|
GRAFANA_PASSWORD=${GRAFANA_PASSWORD}
|
||||||
|
EOF
|
||||||
|
chmod 600 .env.observability
|
||||||
|
|
||||||
|
docker compose --env-file .env.observability -f docker-compose.observability.yml pull
|
||||||
|
docker compose --env-file .env.observability -f docker-compose.observability.yml up -d --remove-orphans
|
||||||
|
docker image prune -f
|
||||||
|
|
||||||
|
echo "✅ netrunner-data observability stack synchronized."
|
||||||
|
echo "⚠️ Не забудьте: firewall на этом VPS должен пускать 3100 (Loki push) ТОЛЬКО с IP backend/landing/каждой прокси-ноды по мере их появления."
|
||||||
|
env:
|
||||||
|
GRAFANA_PASSWORD: ${{ secrets.GRAFANA_PASSWORD }}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: Deploy Observability
|
||||||
|
|
||||||
|
# Отдельный от deploy.yml workflow — Prometheus/Loki/Grafana не должны
|
||||||
|
# рестартовать вместе с Postgres/Vaultwarden (docker-compose.observability.yml
|
||||||
|
# вместо docker-compose.yml, разные жизненные циклы). Деплоится на тот же
|
||||||
|
# DATA_VPS_IP/DATA_SSH_PRIVATE_KEY, что и основной стек — тот же VPS.
|
||||||
|
#
|
||||||
|
# GRAFANA_PASSWORD — единственный настоящий секрет здесь, остальное (список
|
||||||
|
# таргетов, конфиги Loki/promtail/Grafana-datasources) — публичный код
|
||||||
|
# репозитория, не секреты.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy Observability Stack
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Sync compose file + observability configs to VPS
|
||||||
|
uses: appleboy/scp-action@v0.1.7
|
||||||
|
with:
|
||||||
|
host: ${{ vars.DATA_VPS_IP }}
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.DATA_SSH_PRIVATE_KEY }}
|
||||||
|
source: "docker-compose.observability.yml,observability"
|
||||||
|
target: "/root/netrunner-data"
|
||||||
|
strip_components: 0
|
||||||
|
|
||||||
|
- name: Write .env and deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ vars.DATA_VPS_IP }}
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.DATA_SSH_PRIVATE_KEY }}
|
||||||
|
envs: "GRAFANA_PASSWORD"
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
cd /root/netrunner-data
|
||||||
|
|
||||||
|
# Свой .env.observability, НЕ общий .env (тот принадлежит
|
||||||
|
# deploy.yml/docker-compose.yml и перезаписывается им целиком на
|
||||||
|
# каждом своём прогоне — общий файл рассинхронизировал бы стеки).
|
||||||
|
cat > .env.observability <<EOF
|
||||||
|
GRAFANA_PASSWORD=${GRAFANA_PASSWORD}
|
||||||
|
EOF
|
||||||
|
chmod 600 .env.observability
|
||||||
|
|
||||||
|
docker compose --env-file .env.observability -f docker-compose.observability.yml pull
|
||||||
|
docker compose --env-file .env.observability -f docker-compose.observability.yml up -d --remove-orphans
|
||||||
|
docker image prune -f
|
||||||
|
|
||||||
|
echo "✅ netrunner-data observability stack synchronized."
|
||||||
|
echo "⚠️ Не забудьте: firewall на этом VPS должен пускать 3100 (Loki push) ТОЛЬКО с IP backend/landing/каждой прокси-ноды по мере их появления."
|
||||||
|
env:
|
||||||
|
GRAFANA_PASSWORD: ${{ secrets.GRAFANA_PASSWORD }}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# =====================================================================
|
||||||
|
# netrunner-data — стек наблюдаемости (Prometheus + Loki + Grafana),
|
||||||
|
# ОТДЕЛЬНО от docker-compose.yml (Postgres+Vaultwarden+backup) — разный
|
||||||
|
# жизненный цикл: рестарт/редеплой мониторинга не должен трогать БД.
|
||||||
|
#
|
||||||
|
# Топология: этот стек — единственное место сбора для backend, лендинга и
|
||||||
|
# КАЖДОЙ прокси-ноды ("ядро"), которые физически живут на других серверах.
|
||||||
|
# - Prometheus САМ ходит наружу (scrape) в /metrics на тех хостах — тут
|
||||||
|
# ничего дополнительно открывать не нужно, только у НИХ надо будет
|
||||||
|
# открыть порт метрик для IP этого VPS (см. документацию по мере
|
||||||
|
# инструментирования backend/landing/нод).
|
||||||
|
# - Loki, наоборот, принимает пуши логов ИЗВНЕ (тонкий promtail на каждом
|
||||||
|
# удалённом хосте пушит сюда) — порт 3100 публикуется наружу и
|
||||||
|
# ОБЯЗАТЕЛЬНО фильтруется firewall'ом по IP тех хостов (см.
|
||||||
|
# LOKI_ALLOWED_CIDR ниже, заглушка 0.0.0.0/0 — заменить по мере аренды
|
||||||
|
# серверов, тот же паттерн что postgres/pg_hba.conf в соседнем стеке).
|
||||||
|
# - Prometheus/Grafana UI НЕ публикуются напрямую (127.0.0.1-only) — доступ
|
||||||
|
# к Grafana только через Caddy с auth-гейтом (см. фазу 7 плана
|
||||||
|
# мониторинга), Prometheus UI вообще не нужен наружу.
|
||||||
|
#
|
||||||
|
# Список нод для scrape (observability/targets/nodes.json) генерится и
|
||||||
|
# заливается сюда отдельным шагом деплоя netrunner-proxy — см.
|
||||||
|
# netrunner-proxy/scripts/generate-prometheus-targets.mjs. Пустой массив —
|
||||||
|
# нормальное состояние до первой реальной интеграции нод.
|
||||||
|
# =====================================================================
|
||||||
|
services:
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:latest
|
||||||
|
container_name: netrunner-prometheus
|
||||||
|
volumes:
|
||||||
|
- ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||||
|
- ./observability/targets:/etc/prometheus/targets:ro
|
||||||
|
- prometheus_data:/prometheus
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:9090:9090"
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- netrunner_observability
|
||||||
|
|
||||||
|
loki:
|
||||||
|
image: grafana/loki:latest
|
||||||
|
container_name: netrunner-loki
|
||||||
|
volumes:
|
||||||
|
- ./observability/loki-config.yml:/etc/loki/local-config.yaml:ro
|
||||||
|
- loki_data:/tmp/loki
|
||||||
|
ports:
|
||||||
|
# Не 127.0.0.1 — сюда пушат тонкие promtail с других хостов.
|
||||||
|
# Обязательно firewall на конкретные IP, см. шапку файла.
|
||||||
|
- "3100:3100"
|
||||||
|
command: -config.file=/etc/loki/local-config.yaml
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- netrunner_observability
|
||||||
|
|
||||||
|
promtail:
|
||||||
|
image: grafana/promtail:latest
|
||||||
|
container_name: netrunner-observability-promtail
|
||||||
|
volumes:
|
||||||
|
- ./observability/promtail-config.yml:/etc/promtail/config.yml:ro
|
||||||
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
command: -config.file=/etc/promtail/config.yml
|
||||||
|
restart: always
|
||||||
|
user: root
|
||||||
|
depends_on:
|
||||||
|
- loki
|
||||||
|
networks:
|
||||||
|
- netrunner_observability
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
container_name: netrunner-grafana
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3030:3000"
|
||||||
|
environment:
|
||||||
|
- GF_SECURITY_ADMIN_USER=admin
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
|
||||||
|
# allow_embedding нужен для iframe-встройки в React-админку (см.
|
||||||
|
# Caddyfile + frontend/src/ObservabilityPage.tsx) — без него Grafana
|
||||||
|
# сама шлёт X-Frame-Options: deny.
|
||||||
|
- GF_SECURITY_ALLOW_EMBEDDING=true
|
||||||
|
- GF_AUTH_ANONYMOUS_ENABLED=false
|
||||||
|
volumes:
|
||||||
|
- grafana_data:/var/lib/grafana
|
||||||
|
- ./observability/grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml:ro
|
||||||
|
- ./observability/grafana-dashboards-provider.yml:/etc/grafana/provisioning/dashboards/dashboards.yml:ro
|
||||||
|
- ./observability/grafana-dashboards:/etc/grafana/provisioning/dashboards/files:ro
|
||||||
|
depends_on:
|
||||||
|
- prometheus
|
||||||
|
- loki
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- netrunner_observability
|
||||||
|
|
||||||
|
# Единственный публичный вход в Grafana — гейтит доступ через существующую
|
||||||
|
# cookie-сессию netrunner-backend (Owner/Moderator), см. observability/Caddyfile.
|
||||||
|
# Порт Grafana выше (3030) остаётся 127.0.0.1-only — снаружи только через это.
|
||||||
|
caddy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
container_name: netrunner-observability-caddy
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./observability/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- caddy_data:/data
|
||||||
|
- caddy_config:/config
|
||||||
|
depends_on:
|
||||||
|
- grafana
|
||||||
|
networks:
|
||||||
|
- netrunner_observability
|
||||||
|
|
||||||
|
networks:
|
||||||
|
netrunner_observability:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
prometheus_data:
|
||||||
|
loki_data:
|
||||||
|
grafana_data:
|
||||||
|
caddy_data:
|
||||||
|
caddy_config:
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Гейтит доступ к Grafana через уже существующую cookie-сессию
|
||||||
|
# netrunner-backend (владелец/модератор) — Grafana сама наружу НЕ торчит
|
||||||
|
# (см. docker-compose.observability.yml, порт 3030 только на 127.0.0.1),
|
||||||
|
# только через этот Caddy.
|
||||||
|
#
|
||||||
|
# Как это работает: cookie сессии backend'а (nrxp_token) ставится с
|
||||||
|
# Domain=.netrunner-vpn.com (см. COOKIE_DOMAIN в netrunner-backend/.env) —
|
||||||
|
# значит браузер сам приложит её и к запросам на grafana.netrunner-vpn.com
|
||||||
|
# (тот же родительский домен, другой сабдомен — это same-site, не
|
||||||
|
# cross-site, для куки без атрибута Partitioned браузер это разрешает даже
|
||||||
|
# из iframe). forward_auth здесь пересылает эту cookie на реальный бэкенд
|
||||||
|
# (уже на другом VPS) и либо пускает дальше в Grafana (200), либо блокирует
|
||||||
|
# (401) — сам Caddy решения не принимает, только проксирует запрос-проверку.
|
||||||
|
#
|
||||||
|
# ЗАМЕНИТЬ ПЕРЕД ПЕРВЫМ ЗАПУСКОМ:
|
||||||
|
# - CHANGE_ME_GRAFANA_DOMAIN — сабдомен для Grafana (например
|
||||||
|
# grafana.netrunner-vpn.com), DNS A-запись на IP этого VPS.
|
||||||
|
# - CHANGE_ME_BACKEND_PUBLIC_URL — публичный URL бэкенда (например
|
||||||
|
# https://account.netrunner-vpn.com), КОГДА backend будет арендован и
|
||||||
|
# задеплоен — до этого auth-гейт нерабочий (пускать некому проверять).
|
||||||
|
CHANGE_ME_GRAFANA_DOMAIN {
|
||||||
|
forward_auth CHANGE_ME_BACKEND_PUBLIC_URL {
|
||||||
|
uri /api/v1/admin/observability/check
|
||||||
|
copy_headers Cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_proxy grafana:3000
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Provisioning-провайдер для дашбордов — Grafana сама подхватывает любой
|
||||||
|
# .json, положенный в примонтированную папку grafana-dashboards/ (см.
|
||||||
|
# docker-compose.observability.yml), без ручного импорта через UI и без
|
||||||
|
# рестарта контейнера (updateIntervalSeconds перечитывает папку).
|
||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
providers:
|
||||||
|
- name: netrunner
|
||||||
|
orgId: 1
|
||||||
|
folder: "Netrunner"
|
||||||
|
type: file
|
||||||
|
disableDeletion: false
|
||||||
|
updateIntervalSeconds: 30
|
||||||
|
allowUiUpdates: false
|
||||||
|
options:
|
||||||
|
path: /etc/grafana/provisioning/dashboards/files
|
||||||
|
foldersFromFilesStructure: false
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"uid": "netrunner-backend",
|
||||||
|
"title": "Netrunner — Backend",
|
||||||
|
"timezone": "utc",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "30s",
|
||||||
|
"time": { "from": "now-6h", "to": "now" },
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "HTTP requests/sec (by status)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(http_requests_total{job=\"netrunner-backend\"}[5m])) by (status)",
|
||||||
|
"legendFormat": "{{status}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "HTTP latency p95/p50",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job=\"netrunner-backend\"}[5m])) by (le))",
|
||||||
|
"legendFormat": "p95"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket{job=\"netrunner-backend\"}[5m])) by (le))",
|
||||||
|
"legendFormat": "p50"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "Errors/sec (by type)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(app_errors_total{job=\"netrunner-backend\"}[5m])) by (type)",
|
||||||
|
"legendFormat": "{{type}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "Payments/sec (by provider, status)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(payments_total{job=\"netrunner-backend\"}[5m])) by (provider, status)",
|
||||||
|
"legendFormat": "{{provider}}/{{status}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"title": "VPN nodes online / total",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{ "expr": "nodes_online{job=\"netrunner-backend\"}", "legendFormat": "online" },
|
||||||
|
{ "expr": "nodes_total{job=\"netrunner-backend\"}", "legendFormat": "total" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"title": "Support tickets/hour (created vs closed)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(support_tickets_total{job=\"netrunner-backend\"}[1h])) by (status) * 3600",
|
||||||
|
"legendFormat": "{{status}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"title": "Referral signups + partner commission",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 24 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(referral_signups_total{job=\"netrunner-backend\"}[1h])) * 3600",
|
||||||
|
"legendFormat": "referral signups/hour"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(partner_commission_total{job=\"netrunner-backend\"}[1h])) by (currency) * 3600",
|
||||||
|
"legendFormat": "commission/hour {{currency}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"uid": "netrunner-landing",
|
||||||
|
"title": "Netrunner — Landing",
|
||||||
|
"timezone": "utc",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "30s",
|
||||||
|
"time": { "from": "now-6h", "to": "now" },
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Next.js requests/sec (by status)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(http_requests_total{job=\"netrunner-landing\"}[5m])) by (status)",
|
||||||
|
"legendFormat": "{{status}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "ws-proxy (гостевая книга/спидтест) requests/sec",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(http_requests_total{job=\"netrunner-landing-ws-proxy\"}[5m])) by (path, status)",
|
||||||
|
"legendFormat": "{{path}} {{status}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "ws-proxy latency p95/p50",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job=\"netrunner-landing-ws-proxy\"}[5m])) by (le))",
|
||||||
|
"legendFormat": "p95"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket{job=\"netrunner-landing-ws-proxy\"}[5m])) by (le))",
|
||||||
|
"legendFormat": "p50"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "Next.js process (CPU/memory)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "rate(process_cpu_seconds_total{job=\"netrunner-landing\"}[5m])",
|
||||||
|
"legendFormat": "CPU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "process_resident_memory_bytes{job=\"netrunner-landing\"}",
|
||||||
|
"legendFormat": "RSS bytes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"uid": "netrunner-proxy-nodes",
|
||||||
|
"title": "Netrunner — Прокси-ноды (ядро)",
|
||||||
|
"timezone": "utc",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "30s",
|
||||||
|
"time": { "from": "now-6h", "to": "now" },
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"name": "node",
|
||||||
|
"type": "query",
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"query": "label_values(netrunner_connections_active, node)",
|
||||||
|
"includeAll": true,
|
||||||
|
"multi": true,
|
||||||
|
"current": { "text": "All", "value": "$__all" },
|
||||||
|
"refresh": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Активные соединения (по ноде)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "netrunner_connections_active{job=\"netrunner-proxy-nodes\", node=~\"$node\"}",
|
||||||
|
"legendFormat": "{{node}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "Новых соединений/сек (по ноде)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(netrunner_connections_total{job=\"netrunner-proxy-nodes\", node=~\"$node\"}[5m])) by (node)",
|
||||||
|
"legendFormat": "{{node}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "Auth validate latency p95 (по ноде)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "histogram_quantile(0.95, sum(rate(netrunner_auth_validate_duration_seconds_bucket{job=\"netrunner-proxy-nodes\", node=~\"$node\"}[5m])) by (le, node))",
|
||||||
|
"legendFormat": "{{node}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "Auth failures/sec (по ноде)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(netrunner_auth_failures_total{job=\"netrunner-proxy-nodes\", node=~\"$node\"}[5m])) by (node)",
|
||||||
|
"legendFormat": "{{node}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"title": "Circuit breaker открыт (0/1, по ноде)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 16 },
|
||||||
|
"datasource": { "type": "prometheus", "uid": "Prometheus" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "netrunner_circuit_breaker_open{job=\"netrunner-proxy-nodes\", node=~\"$node\"}",
|
||||||
|
"legendFormat": "{{node}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Prometheus
|
||||||
|
type: prometheus
|
||||||
|
access: proxy
|
||||||
|
url: http://prometheus:9090
|
||||||
|
isDefault: true
|
||||||
|
version: 1
|
||||||
|
editable: true
|
||||||
|
|
||||||
|
- name: Loki
|
||||||
|
type: loki
|
||||||
|
access: proxy
|
||||||
|
url: http://loki:3100
|
||||||
|
version: 1
|
||||||
|
editable: true
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
auth_enabled: false
|
||||||
|
|
||||||
|
server:
|
||||||
|
http_listen_port: 3100
|
||||||
|
grpc_listen_port: 9096
|
||||||
|
|
||||||
|
common:
|
||||||
|
instance_addr: 127.0.0.1
|
||||||
|
path_prefix: /tmp/loki
|
||||||
|
storage:
|
||||||
|
filesystem:
|
||||||
|
chunks_directory: /tmp/loki/chunks
|
||||||
|
rules_directory: /tmp/loki/rules
|
||||||
|
replication_factor: 1
|
||||||
|
ring:
|
||||||
|
kvstore:
|
||||||
|
store: inmemory
|
||||||
|
|
||||||
|
query_range:
|
||||||
|
results_cache:
|
||||||
|
cache:
|
||||||
|
embedded_cache:
|
||||||
|
enabled: true
|
||||||
|
max_size_mb: 100
|
||||||
|
|
||||||
|
schema_config:
|
||||||
|
configs:
|
||||||
|
- from: 2020-10-24
|
||||||
|
store: tsdb
|
||||||
|
object_store: filesystem
|
||||||
|
schema: v13
|
||||||
|
index:
|
||||||
|
prefix: index_
|
||||||
|
period: 24h
|
||||||
|
|
||||||
|
ruler:
|
||||||
|
alertmanager_url: http://localhost:9093
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Скрейп-конфиг центрального Prometheus (стоит на этом же VPS, рядом с
|
||||||
|
# Postgres/Vaultwarden — см. docker-compose.observability.yml).
|
||||||
|
#
|
||||||
|
# Прокси-ноды (netrunner-proxy-nodes) — уже реальный динамический список:
|
||||||
|
# targets/nodes.json генерится и заливается сюда автоматически при каждом
|
||||||
|
# деплое нод (netrunner-proxy/scripts/generate-prometheus-targets.mjs,
|
||||||
|
# .gitea/workflows/deploy.yml::update-observability-targets).
|
||||||
|
#
|
||||||
|
# backend/landing уже инструментированы (/metrics — см. src/api.rs в
|
||||||
|
# netrunner-backend, app/api/metrics/route.ts + proxy-ws/src/main.rs в
|
||||||
|
# netrunner-landing) — таргеты ниже закомментированы только потому, что
|
||||||
|
# backend ещё не арендован (нет реального хоста), а для landing нужно
|
||||||
|
# подставить актуальный IP её VPS. Раскомментировать и подставить реальные
|
||||||
|
# значения, как только они известны — рестарт контейнера НЕ нужен для
|
||||||
|
# file_sd_configs, но ДЛЯ static_configs (эти два таргета) правки в
|
||||||
|
# prometheus.yml требуют `docker compose restart prometheus` на этом VPS.
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: "prometheus"
|
||||||
|
static_configs:
|
||||||
|
- targets: ["localhost:9090"]
|
||||||
|
|
||||||
|
# Прокси-ноды: динамический список, file_sd_configs перечитывается
|
||||||
|
# автоматически при изменении файла, рестарт Prometheus не нужен.
|
||||||
|
- job_name: "netrunner-proxy-nodes"
|
||||||
|
file_sd_configs:
|
||||||
|
- files:
|
||||||
|
- "/etc/prometheus/targets/nodes.json"
|
||||||
|
refresh_interval: 30s
|
||||||
|
|
||||||
|
# - job_name: "netrunner-backend"
|
||||||
|
# static_configs:
|
||||||
|
# - targets: ["CHANGE_ME_BACKEND_HOST:8080"]
|
||||||
|
# - job_name: "netrunner-landing"
|
||||||
|
# static_configs:
|
||||||
|
# - targets: ["CHANGE_ME_LANDING_HOST:3000"] # Next.js /api/metrics
|
||||||
|
# - job_name: "netrunner-landing-ws-proxy"
|
||||||
|
# static_configs:
|
||||||
|
# - targets: ["CHANGE_ME_LANDING_HOST:3001"] # proxy-ws /metrics
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Собирает докер-логи ТОЛЬКО этого хоста (Postgres/Vaultwarden/backup из
|
||||||
|
# netrunner-data/docker-compose.yml + prometheus/loki/grafana этого же
|
||||||
|
# compose — docker_sd_configs видит все контейнеры хоста через общий
|
||||||
|
# docker.sock, не только контейнеры этого конкретного compose-проекта).
|
||||||
|
#
|
||||||
|
# Логи с backend/landing/каждой прокси-ноды приходят СЮДА ЖЕ по сети —
|
||||||
|
# на каждом из тех хостов свой отдельный тонкий promtail, пушащий прямо в
|
||||||
|
# Loki на этом VPS (см. netrunner-backend/promtail-config.yml после
|
||||||
|
# переноса — `clients.url` там указывает на публичный адрес этого хоста).
|
||||||
|
server:
|
||||||
|
http_listen_port: 9080
|
||||||
|
grpc_listen_port: 0
|
||||||
|
|
||||||
|
positions:
|
||||||
|
filename: /tmp/positions.yaml
|
||||||
|
|
||||||
|
clients:
|
||||||
|
- url: http://loki:3100/loki/api/v1/push
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: docker_service_logs
|
||||||
|
docker_sd_configs:
|
||||||
|
- host: unix:///var/run/docker.sock
|
||||||
|
refresh_interval: 5s
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels: ["__meta_docker_container_name"]
|
||||||
|
regex: "/(.*)"
|
||||||
|
target_label: "container"
|
||||||
|
pipeline_stages:
|
||||||
|
- docker: {}
|
||||||
|
- json:
|
||||||
|
expressions:
|
||||||
|
level: level
|
||||||
|
module: target
|
||||||
|
message: message
|
||||||
|
- labels:
|
||||||
|
level:
|
||||||
|
module:
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
Reference in New Issue
Block a user