Единый мониторинг: бизнес-метрики + вынос Prometheus/Loki/Grafana на VPS с данными
Prometheus/Loki/Grafana больше не живут внутри backend-стека — единый
стек теперь на VPS с данными (netrunner-data), т.к. ему нужно видеть
ещё лендинг и все прокси-ноды, не только бэкенд. Отсюда только тонкий
promtail, пушащий логи по сети (LOKI_PUSH_URL).
Новые бизнес-метрики поверх уже существующих generic HTTP-метрик:
payments_total{provider,status}, nodes_online/nodes_total,
support_tickets_total{status}, referral_signups_total,
partner_commission_total{currency} — тем же макросом metrics::counter!/
gauge!, что уже был в api.rs/error.rs.
Новый гейт-эндпоинт GET /api/v1/admin/observability/check (только
Owner/Moderator) — для Caddy forward_auth перед Grafana (см.
netrunner-data/observability/Caddyfile), встройка в админку через
GRAFANA_PUBLIC_URL в GET /api/v1/config.
templates/init_node.sh — добавляет --metrics-port 9093 в провижининг
новых прокси-нод.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+18
-1
@@ -32,13 +32,30 @@ REDIS_URL=redis://redis:6379
|
|||||||
# =====================================================================
|
# =====================================================================
|
||||||
JWT_SECRET=CHANGE_ME_JWT_SECRET_openssl_rand_hex_32
|
JWT_SECRET=CHANGE_ME_JWT_SECRET_openssl_rand_hex_32
|
||||||
ARGON_SALT=CHANGE_ME_ARGON_SALT_MIN_16_CHARS
|
ARGON_SALT=CHANGE_ME_ARGON_SALT_MIN_16_CHARS
|
||||||
GRAFANA_PASSWORD=CHANGE_ME_GRAFANA_PASSWORD
|
|
||||||
TON_MASTER_WALLET=UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKK8y
|
TON_MASTER_WALLET=UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKK8y
|
||||||
|
|
||||||
# Бэкапы Postgres (pg_dump + опциональный rclone) теперь настраиваются в
|
# Бэкапы Postgres (pg_dump + опциональный rclone) теперь настраиваются в
|
||||||
# netrunner-data/.env (там же, где физически стоит БД) — см.
|
# netrunner-data/.env (там же, где физически стоит БД) — см.
|
||||||
# netrunner-data/scripts/backup.sh. Здесь их больше нет.
|
# netrunner-data/scripts/backup.sh. Здесь их больше нет.
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# === НАБЛЮДАЕМОСТЬ ===
|
||||||
|
# =====================================================================
|
||||||
|
# Prometheus/Loki/Grafana теперь единым стеком на VPS с данными (см.
|
||||||
|
# netrunner-data/docker-compose.observability.yml) — GRAFANA_PASSWORD
|
||||||
|
# больше не нужен здесь. Этот бэкенд только пушит логи туда через тонкий
|
||||||
|
# promtail (сервис `promtail` в docker-compose.prod.yml) — публичный адрес
|
||||||
|
# того VPS, порт 3100 (обязательно зафайрволенный там на IP этого хоста).
|
||||||
|
LOKI_PUSH_URL=http://CHANGE_ME_DATA_VPS_IP:3100
|
||||||
|
|
||||||
|
# Публичный URL Grafana за auth-гейтом Caddy (см.
|
||||||
|
# netrunner-data/observability/Caddyfile) — отдаётся фронту через
|
||||||
|
# GET /api/v1/config для встройки в ObservabilityPage.tsx (frontend/src/).
|
||||||
|
# Не задан — страница просто не показывает дашборды. Ставить ТОЛЬКО после
|
||||||
|
# того, как в Caddyfile настоящий backend-URL вместо CHANGE_ME_BACKEND_PUBLIC_URL —
|
||||||
|
# иначе будет вести на нерабочий auth-гейт.
|
||||||
|
GRAFANA_PUBLIC_URL=https://CHANGE_ME_GRAFANA_DOMAIN
|
||||||
|
|
||||||
# Опционально: без него оплата через @CryptoBot вернёт "provider not
|
# Опционально: без него оплата через @CryptoBot вернёт "provider not
|
||||||
# supported", остальные провайдеры продолжают работать. Токен — у @CryptoBot:
|
# supported", остальные провайдеры продолжают работать. Токен — у @CryptoBot:
|
||||||
# /pay -> Create App -> Payment Token.
|
# /pay -> Create App -> Payment Token.
|
||||||
|
|||||||
+12
-48
@@ -62,67 +62,31 @@ services:
|
|||||||
- netrunner_grid
|
- netrunner_grid
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# МОНИТОРИНГ
|
# НАБЛЮДАЕМОСТЬ
|
||||||
# ==========================================
|
# ==========================================
|
||||||
prometheus:
|
# Prometheus/Loki/Grafana больше НЕ здесь — единый стек теперь на VPS с
|
||||||
image: prom/prometheus:latest
|
# данными (netrunner-data/docker-compose.observability.yml), т.к. ему
|
||||||
container_name: netrunner-prometheus
|
# всё равно нужно видеть лендинг и все прокси-ноды, не только бэкенд.
|
||||||
volumes:
|
# /metrics бэкенда (см. src/api.rs) — тот central Prometheus скрейпит
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
# удалённо; отсюда только пуш логов через тонкий promtail.
|
||||||
ports:
|
|
||||||
- "127.0.0.1:9090:9090"
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- netrunner_grid
|
|
||||||
|
|
||||||
loki:
|
|
||||||
image: grafana/loki:latest
|
|
||||||
container_name: netrunner-loki
|
|
||||||
volumes:
|
|
||||||
- ./loki-config.yml:/etc/loki/local-config.yaml:ro
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:3100:3100"
|
|
||||||
command: -config.file=/etc/loki/local-config.yaml
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- netrunner_grid
|
|
||||||
|
|
||||||
promtail:
|
promtail:
|
||||||
image: grafana/promtail:latest
|
image: grafana/promtail:latest
|
||||||
container_name: netrunner-promtail
|
container_name: netrunner-promtail
|
||||||
|
environment:
|
||||||
|
- LOKI_PUSH_URL=${LOKI_PUSH_URL}
|
||||||
volumes:
|
volumes:
|
||||||
- ./promtail-config.yml:/etc/promtail/config.yml:ro
|
- ./promtail-config.yml:/etc/promtail/config.yml:ro
|
||||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
command: -config.file=/etc/promtail/config.yml
|
# -config.expand-env: без него promtail не подставит ${LOKI_PUSH_URL}
|
||||||
|
# внутри самого config.yml — сама переменная передаётся выше через
|
||||||
|
# environment, файл конфига читает её только с этим флагом.
|
||||||
|
command: -config.file=/etc/promtail/config.yml -config.expand-env=true
|
||||||
restart: always
|
restart: always
|
||||||
user: root
|
user: root
|
||||||
depends_on:
|
|
||||||
- loki
|
|
||||||
networks:
|
|
||||||
- netrunner_grid
|
|
||||||
|
|
||||||
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}
|
|
||||||
volumes:
|
|
||||||
- grafana_data:/var/lib/grafana
|
|
||||||
- ./grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml:ro
|
|
||||||
depends_on:
|
|
||||||
- prometheus
|
|
||||||
- loki
|
|
||||||
restart: always
|
|
||||||
networks:
|
networks:
|
||||||
- netrunner_grid
|
- netrunner_grid
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
netrunner_grid:
|
netrunner_grid:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
|
||||||
grafana_data:
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
global:
|
|
||||||
scrape_interval: 15s # Как часто опрашивать бэкенд
|
|
||||||
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: "netrunner-backend"
|
|
||||||
static_configs:
|
|
||||||
- targets: ["app:8080"] # Адрес твоего Rust-бэкенда
|
|
||||||
+5
-1
@@ -1,3 +1,7 @@
|
|||||||
|
# Тонкий promtail — шлёт логи ЭТОГО хоста (app/migrate/redis) в центральный
|
||||||
|
# Loki на VPS с данными (см. netrunner-data/docker-compose.observability.yml).
|
||||||
|
# LOKI_PUSH_URL — публичный адрес того VPS, порт 3100, обязательно
|
||||||
|
# зафайрволенный там на IP этого хоста (см. шапку docker-compose.observability.yml).
|
||||||
server:
|
server:
|
||||||
http_listen_port: 9080
|
http_listen_port: 9080
|
||||||
grpc_listen_port: 0
|
grpc_listen_port: 0
|
||||||
@@ -6,7 +10,7 @@ positions:
|
|||||||
filename: /tmp/positions.yaml
|
filename: /tmp/positions.yaml
|
||||||
|
|
||||||
clients:
|
clients:
|
||||||
- url: http://loki:3100/loki/api/v1/push
|
- url: ${LOKI_PUSH_URL}/loki/api/v1/push
|
||||||
|
|
||||||
scrape_configs:
|
scrape_configs:
|
||||||
- job_name: docker_service_logs
|
- job_name: docker_service_logs
|
||||||
|
|||||||
+17
-2
@@ -32,6 +32,7 @@ use crate::modules::{
|
|||||||
proxy_internal,
|
proxy_internal,
|
||||||
referrals::{controller as ref_ctrl, service::ReferralService},
|
referrals::{controller as ref_ctrl, service::ReferralService},
|
||||||
staff::{controller as staff_ctrl, service::StaffService},
|
staff::{controller as staff_ctrl, service::StaffService},
|
||||||
|
support::{controller as support_ctrl, service::SupportService},
|
||||||
users::{controller as user_ctrl, service::UserService},
|
users::{controller as user_ctrl, service::UserService},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ pub struct ApiState {
|
|||||||
pub referral_service: ReferralService,
|
pub referral_service: ReferralService,
|
||||||
pub user_service: UserService,
|
pub user_service: UserService,
|
||||||
pub staff_service: StaffService,
|
pub staff_service: StaffService,
|
||||||
|
pub support_service: SupportService,
|
||||||
pub jwt_secret: String,
|
pub jwt_secret: String,
|
||||||
pub bot_token: String,
|
pub bot_token: String,
|
||||||
/// Юзернейм бота (без `@`) — отдаётся фронту через `GET /api/v1/config`.
|
/// Юзернейм бота (без `@`) — отдаётся фронту через `GET /api/v1/config`.
|
||||||
@@ -84,6 +86,7 @@ impl Clone for ApiState {
|
|||||||
referral_service: self.referral_service.clone(),
|
referral_service: self.referral_service.clone(),
|
||||||
user_service: self.user_service.clone(),
|
user_service: self.user_service.clone(),
|
||||||
staff_service: self.staff_service.clone(),
|
staff_service: self.staff_service.clone(),
|
||||||
|
support_service: self.support_service.clone(),
|
||||||
jwt_secret: self.jwt_secret.clone(),
|
jwt_secret: self.jwt_secret.clone(),
|
||||||
bot_token: self.bot_token.clone(),
|
bot_token: self.bot_token.clone(),
|
||||||
bot_username: self.bot_username.clone(),
|
bot_username: self.bot_username.clone(),
|
||||||
@@ -144,7 +147,12 @@ pub fn create_router(state: ApiState, frontend_dir: &str) -> Router {
|
|||||||
.nest("/staff", staff_ctrl::router(state.clone()))
|
.nest("/staff", staff_ctrl::router(state.clone()))
|
||||||
.nest("/staff", staff_ctrl::owner_router(state.clone()))
|
.nest("/staff", staff_ctrl::owner_router(state.clone()))
|
||||||
.nest("/staff", staff_ctrl::staff_router(state.clone()))
|
.nest("/staff", staff_ctrl::staff_router(state.clone()))
|
||||||
.nest("/partners", staff_ctrl::partner_router(state.clone()));
|
.nest("/partners", staff_ctrl::partner_router(state.clone()))
|
||||||
|
.nest("/support", support_ctrl::router(state.clone()))
|
||||||
|
.nest(
|
||||||
|
"/observability",
|
||||||
|
staff_ctrl::observability_router(state.clone()),
|
||||||
|
);
|
||||||
|
|
||||||
// Внутренний контракт для прокси-нод — отдельный секрет, не auth_guard
|
// Внутренний контракт для прокси-нод — отдельный секрет, не auth_guard
|
||||||
// (прокси не пользовательская сессия) и не участвует в CORS ниже (никогда
|
// (прокси не пользовательская сессия) и не участвует в CORS ниже (никогда
|
||||||
@@ -201,7 +209,14 @@ pub fn create_router(state: ApiState, frontend_dir: &str) -> Router {
|
|||||||
/// Публичная (без auth_guard) рантайм-конфигурация для статического SPA —
|
/// Публичная (без auth_guard) рантайм-конфигурация для статического SPA —
|
||||||
/// см. `ApiState::bot_username` про то, почему это не build-time env фронта.
|
/// см. `ApiState::bot_username` про то, почему это не build-time env фронта.
|
||||||
async fn get_public_config(State(state): State<ApiState>) -> impl IntoResponse {
|
async fn get_public_config(State(state): State<ApiState>) -> impl IntoResponse {
|
||||||
Json(json!({ "bot_username": state.bot_username }))
|
// Публичный URL Grafana (за auth-гейтом Caddy, см.
|
||||||
|
// netrunner-data/observability/Caddyfile) — тот же паттерн, что и
|
||||||
|
// bot_username выше: читается на сервере заново на каждый запрос, а не
|
||||||
|
// зашивается в SPA-бандл на этапе сборки, т.к. разный на разных стендах.
|
||||||
|
// `None`, пока GRAFANA_PUBLIC_URL не задан — ObservabilityPage.tsx в
|
||||||
|
// этом случае просто не показывает ссылку/iframe.
|
||||||
|
let grafana_url = std::env::var("GRAFANA_PUBLIC_URL").ok();
|
||||||
|
Json(json!({ "bot_username": state.bot_username, "grafana_url": grafana_url }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Liveness: процесс жив и отвечает. Не трогает зависимости.
|
/// Liveness: процесс жив и отвечает. Не трогает зависимости.
|
||||||
|
|||||||
@@ -492,6 +492,7 @@ impl AuthService {
|
|||||||
.create_referral(referrer.id, new_user_id)
|
.create_referral(referrer.id, new_user_id)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
metrics::counter!("referral_signups_total").increment(1);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,6 +214,8 @@ impl BillingService {
|
|||||||
.repo
|
.repo
|
||||||
.create_invoice(user.id, plan_name, provider_name, currency, invoice_amount)
|
.create_invoice(user.id, plan_name, provider_name, currency, invoice_amount)
|
||||||
.await?;
|
.await?;
|
||||||
|
metrics::counter!("payments_total", "provider" => provider_name.to_string(), "status" => "initiated")
|
||||||
|
.increment(1);
|
||||||
|
|
||||||
let invoice_data = InvoiceData {
|
let invoice_data = InvoiceData {
|
||||||
invoice_id,
|
invoice_id,
|
||||||
@@ -273,6 +275,8 @@ impl BillingService {
|
|||||||
self.repo
|
self.repo
|
||||||
.update_invoice_status(invoice_id, "failed", Some(external_tx_id))
|
.update_invoice_status(invoice_id, "failed", Some(external_tx_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
metrics::counter!("payments_total", "provider" => invoice.provider.clone(), "status" => "failed")
|
||||||
|
.increment(1);
|
||||||
return Err(AppError::BusinessLogic(
|
return Err(AppError::BusinessLogic(
|
||||||
"Payment verification failed".into(),
|
"Payment verification failed".into(),
|
||||||
));
|
));
|
||||||
@@ -282,6 +286,8 @@ impl BillingService {
|
|||||||
self.repo
|
self.repo
|
||||||
.update_invoice_status(invoice_id, "paid", Some(external_tx_id))
|
.update_invoice_status(invoice_id, "paid", Some(external_tx_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
metrics::counter!("payments_total", "provider" => invoice.provider.clone(), "status" => "paid")
|
||||||
|
.increment(1);
|
||||||
|
|
||||||
// 5. АКТИВИРУЕМ ТАРИФ
|
// 5. АКТИВИРУЕМ ТАРИФ
|
||||||
self.repo
|
self.repo
|
||||||
@@ -360,6 +366,8 @@ impl BillingService {
|
|||||||
percent,
|
percent,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
metrics::counter!("partner_commission_total", "currency" => invoice.currency.clone())
|
||||||
|
.increment(commission as u64);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Extension, Path, State},
|
extract::{Extension, Path, State},
|
||||||
|
http::StatusCode,
|
||||||
middleware,
|
middleware,
|
||||||
routing::{get, patch, post},
|
routing::{get, patch, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
@@ -27,10 +28,24 @@ pub fn router(state: ApiState) -> Router<ApiState> {
|
|||||||
.route_layer(middleware::from_fn_with_state(state, auth_guard))
|
.route_layer(middleware::from_fn_with_state(state, auth_guard))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Только Owner: создание модераторов, выдача им права на управление нодами.
|
/// Только Owner/Moderator — гейт для Caddy `forward_auth` перед Grafana
|
||||||
|
/// (см. netrunner-data/observability/Caddyfile). Тело ответа не важно, важен
|
||||||
|
/// только статус: 200 — Caddy пускает дальше в Grafana, 401 — блокирует.
|
||||||
|
/// Бизнес-метрики (выручка, число юзеров) чувствительны — Support/Partner
|
||||||
|
/// сюда не допускаются, поэтому `staff_guard`, а не `support_access_guard`.
|
||||||
|
pub fn observability_router(state: ApiState) -> Router<ApiState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/check", get(|| async { StatusCode::OK }))
|
||||||
|
.route_layer(middleware::from_fn_with_state(state.clone(), staff_guard))
|
||||||
|
.route_layer(middleware::from_fn_with_state(state, auth_guard))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Только Owner: создание модераторов/саппортов, выдача модераторам права
|
||||||
|
/// на управление нодами.
|
||||||
pub fn owner_router(state: ApiState) -> Router<ApiState> {
|
pub fn owner_router(state: ApiState) -> Router<ApiState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/moderators", post(create_moderator))
|
.route("/moderators", post(create_moderator))
|
||||||
|
.route("/support", post(create_support))
|
||||||
.route("/users/{id}/permissions", patch(set_permissions))
|
.route("/users/{id}/permissions", patch(set_permissions))
|
||||||
.route_layer(middleware::from_fn_with_state(state.clone(), owner_guard))
|
.route_layer(middleware::from_fn_with_state(state.clone(), owner_guard))
|
||||||
.route_layer(middleware::from_fn_with_state(state, auth_guard))
|
.route_layer(middleware::from_fn_with_state(state, auth_guard))
|
||||||
@@ -89,6 +104,27 @@ async fn create_moderator(
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateSupportPayload {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_support(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
Json(p): Json<CreateSupportPayload>,
|
||||||
|
) -> Result<Json<Value>, AppError> {
|
||||||
|
let user = state
|
||||||
|
.staff_service
|
||||||
|
.create_support(&p.username, &p.password)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(json!({
|
||||||
|
"id": user.id,
|
||||||
|
"username": user.username,
|
||||||
|
"role": user.role,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CreatePartnerPayload {
|
pub struct CreatePartnerPayload {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ async fn run_node_health_check(repo: &Arc<dyn AppRepository>) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let total = nodes.len();
|
||||||
|
let mut online_count: u64 = 0;
|
||||||
|
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
if node.status == "provisioning" {
|
if node.status == "provisioning" {
|
||||||
continue;
|
continue;
|
||||||
@@ -102,6 +105,9 @@ async fn run_node_health_check(repo: &Arc<dyn AppRepository>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let new_status = if is_reachable { "online" } else { "offline" };
|
let new_status = if is_reachable { "online" } else { "offline" };
|
||||||
|
if is_reachable {
|
||||||
|
online_count += 1;
|
||||||
|
}
|
||||||
if new_status != node.status {
|
if new_status != node.status {
|
||||||
info!(node_id = %node.id, name = %node.name, from = %node.status, to = %new_status, "Смена статуса ноды по health-check");
|
info!(node_id = %node.id, name = %node.name, from = %node.status, to = %new_status, "Смена статуса ноды по health-check");
|
||||||
}
|
}
|
||||||
@@ -110,4 +116,7 @@ async fn run_node_health_check(repo: &Arc<dyn AppRepository>) {
|
|||||||
error!(node_id = %node.id, error = ?e, "Health-check: не удалось обновить статус ноды");
|
error!(node_id = %node.id, error = ?e, "Health-check: не удалось обновить статус ноды");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metrics::gauge!("nodes_online").set(online_count as f64);
|
||||||
|
metrics::gauge!("nodes_total").set(total as f64);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ docker run -d \
|
|||||||
--decoy-host "$SNI_DOMAIN" \
|
--decoy-host "$SNI_DOMAIN" \
|
||||||
--require-auth \
|
--require-auth \
|
||||||
--backend-url "$BACKEND_URL" \
|
--backend-url "$BACKEND_URL" \
|
||||||
--health-port 9091
|
--health-port 9091 \
|
||||||
|
--metrics-port 9093
|
||||||
|
|
||||||
echo "[*] Step 6: Starting Watchtower for auto-updates..."
|
echo "[*] Step 6: Starting Watchtower for auto-updates..."
|
||||||
docker run -d \
|
docker run -d \
|
||||||
|
|||||||
Reference in New Issue
Block a user