Метрики /metrics для центрального Prometheus + генерация scrape-таргетов
--metrics-port (0.0.0.0, в отличие от --health-port — тот 127.0.0.1-only) отдаёт netrunner_connections_active/total, netrunner_auth_validate_duration_seconds, netrunner_auth_failures_total, netrunner_circuit_breaker_open. Тот же hand-rolled HTTP-паттерн, что и health.rs — без HTTP-фреймворка ради одного эндпоинта. scripts/generate-prometheus-targets.mjs генерит список scrape-таргетов из nodes.json — job update-observability-targets в deploy.yml гоняет его при каждом деплое нод и заливает на VPS с данными (netrunner-data/observability/targets/nodes.json), чтобы центральный Prometheus не терял из виду ноды без ручной правки конфига. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,3 +15,5 @@ serde = { version = "1", features = ["derive"] }
|
||||
reqwest = { version = "0.13.2", features = ["json"] }
|
||||
async-trait = "0.1.89"
|
||||
dashmap = "6.1.0"
|
||||
metrics = "0.24.3"
|
||||
metrics-exporter-prometheus = { version = "0.18.1", default-features = false }
|
||||
|
||||
@@ -5,7 +5,7 @@ After=network.target
|
||||
[Service]
|
||||
User=root
|
||||
WorkingDirectory=/root/netr-core
|
||||
ExecStart=/root/netr-core/netrunner-server --host=0.0.0.0 --port=8443 --health-port=9092
|
||||
ExecStart=/root/netr-core/netrunner-server --host=0.0.0.0 --port=8443 --health-port=9092 --metrics-port=9094
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
LimitNOFILE=65535
|
||||
|
||||
@@ -5,7 +5,7 @@ After=network.target
|
||||
[Service]
|
||||
User=root
|
||||
WorkingDirectory=/root/netr-core
|
||||
ExecStart=/root/netr-core/netrunner-server --host=0.0.0.0 --port=443 --health-port=9091
|
||||
ExecStart=/root/netr-core/netrunner-server --host=0.0.0.0 --port=443 --health-port=9091 --metrics-port=9093
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
LimitNOFILE=65535
|
||||
|
||||
@@ -62,6 +62,7 @@ impl Circuit {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.consecutive_failures = 0;
|
||||
state.open_until = None;
|
||||
metrics::gauge!("netrunner_circuit_breaker_open").set(0.0);
|
||||
}
|
||||
|
||||
/// Считать неудачей только сетевые ошибки/таймауты/5xx — HTTP 401/403 на
|
||||
@@ -71,6 +72,7 @@ impl Circuit {
|
||||
state.consecutive_failures += 1;
|
||||
if state.consecutive_failures >= FAILURE_THRESHOLD {
|
||||
state.open_until = Some(Instant::now() + CIRCUIT_OPEN_COOLDOWN);
|
||||
metrics::gauge!("netrunner_circuit_breaker_open").set(1.0);
|
||||
warn!(
|
||||
failures = state.consecutive_failures,
|
||||
"Circuit breaker разомкнут: бэкенд не отвечает {} раз подряд",
|
||||
@@ -140,6 +142,7 @@ struct UsageResponse {
|
||||
impl AuthValidator for BackendClient {
|
||||
async fn validate(&self, token: &str) -> Result<UserQuota, AppError> {
|
||||
if token.is_empty() {
|
||||
metrics::counter!("netrunner_auth_failures_total").increment(1);
|
||||
return Err(AppError::new(
|
||||
netrunner_logger::ERR_AUTH_FAILED,
|
||||
"Доступ запрещен",
|
||||
@@ -158,6 +161,7 @@ impl AuthValidator for BackendClient {
|
||||
return Err(Self::circuit_open_error());
|
||||
}
|
||||
|
||||
let request_start = Instant::now();
|
||||
let resp = self
|
||||
.http
|
||||
.post(format!("{}/api/v1/internal/validate", self.base_url))
|
||||
@@ -173,6 +177,8 @@ impl AuthValidator for BackendClient {
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
metrics::histogram!("netrunner_auth_validate_duration_seconds")
|
||||
.record(request_start.elapsed().as_secs_f64());
|
||||
|
||||
// 5xx — признак нездоровья самого бэкенда, считается неудачей для
|
||||
// circuit breaker'а. 4xx (например 401 на невалидный токен) — это
|
||||
@@ -181,6 +187,7 @@ impl AuthValidator for BackendClient {
|
||||
self.circuit.record_failure();
|
||||
}
|
||||
if !resp.status().is_success() {
|
||||
metrics::counter!("netrunner_auth_failures_total").increment(1);
|
||||
return Err(AppError::new(
|
||||
netrunner_logger::ERR_AUTH_FAILED,
|
||||
"Доступ запрещен",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
//! [`ServerDiagnosticsLogger::snapshot_all_sessions`]).
|
||||
|
||||
use netrunner_core::net::diagnostics::{
|
||||
self, DiagnosticsEvent, DiagnosticsSnapshot, DiagnosticsStore, TunnelMetrics,
|
||||
current_timestamp_ms,
|
||||
self, current_timestamp_ms, DiagnosticsEvent, DiagnosticsSnapshot, DiagnosticsStore,
|
||||
TunnelMetrics,
|
||||
};
|
||||
use netrunner_core::net::SessionManager;
|
||||
use netrunner_logger::{error, info, warn};
|
||||
@@ -192,10 +192,7 @@ impl ServerDiagnosticsLogger {
|
||||
let mut rotated = self.log_path.clone();
|
||||
rotated.set_extension("jsonl.1");
|
||||
tokio::fs::rename(&self.log_path, &rotated).await?;
|
||||
info!(
|
||||
"Diagnostics log rotated → {}",
|
||||
rotated.display()
|
||||
);
|
||||
info!("Diagnostics log rotated → {}", rotated.display());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
+26
-1
@@ -13,6 +13,7 @@
|
||||
mod backend_client;
|
||||
mod diagnostics;
|
||||
mod health;
|
||||
mod metrics_server;
|
||||
mod network;
|
||||
use clap::Parser;
|
||||
use netrunner_core::net::AuthValidator;
|
||||
@@ -88,6 +89,13 @@ struct Args {
|
||||
/// этой же машине). Не задан — health-эндпоинт выключен.
|
||||
#[arg(long)]
|
||||
health_port: Option<u16>,
|
||||
|
||||
/// Порт для `/metrics` (Prometheus text exposition) — в отличие от
|
||||
/// `--health-port`, биндится на 0.0.0.0 (нужен для скрейпа удалённым
|
||||
/// центральным Prometheus), ОБЯЗАТЕЛЬНО зафайрволить на IP
|
||||
/// observability-VPS. Не задан — метрики выключены.
|
||||
#[arg(long)]
|
||||
metrics_port: Option<u16>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -106,8 +114,15 @@ fn main() {
|
||||
None
|
||||
};
|
||||
|
||||
// Регистрируется один раз, до первого metrics::counter!/gauge!/histogram! —
|
||||
// если --metrics-port не задан, вызовы макросов молча уходят в
|
||||
// no-op recorder по умолчанию (штатное поведение крейта metrics).
|
||||
let metrics_handle = args
|
||||
.metrics_port
|
||||
.map(|_| metrics_server::install_recorder());
|
||||
|
||||
let net = Network::new(
|
||||
args.host,
|
||||
args.host.clone(),
|
||||
args.port,
|
||||
args.decoy_host,
|
||||
auth,
|
||||
@@ -128,6 +143,16 @@ fn main() {
|
||||
net.run(run_token).await;
|
||||
});
|
||||
|
||||
if let (Some(port), Some(handle)) = (args.metrics_port, metrics_handle) {
|
||||
let metrics_token = token.clone();
|
||||
tokio::spawn(metrics_server::run(
|
||||
"0.0.0.0".to_string(),
|
||||
port,
|
||||
handle,
|
||||
metrics_token,
|
||||
));
|
||||
}
|
||||
|
||||
shutdown_signal(token).await;
|
||||
if let Err(e) = run_handle.await {
|
||||
error!(error = ?e, "Задача сервера завершилась с паникой при остановке");
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//! `/metrics` для центрального Prometheus (см.
|
||||
//! `netrunner-data/docker-compose.observability.yml`) — в отличие от
|
||||
//! `/health` (127.0.0.1-only, см. `health.rs`), этот порт публикуется наружу
|
||||
//! и ОБЯЗАТЕЛЬНО должен быть зафайрволен на IP observability-VPS: любой,
|
||||
//! кто до него дотянется, увидит число активных соединений/трафик этой
|
||||
//! ноды (не секрет пользователей, но разведка для DPI/блокировщика).
|
||||
//!
|
||||
//! Тот же hand-rolled HTTP-паттерн, что и в `health.rs` (не тянуть HTTP-
|
||||
//! фреймворк ради одного эндпоинта на бинарнике, для которого важен размер
|
||||
//! и минимальные зависимости) — просто рендерит текущий снапшот метрик из
|
||||
//! уже установленного `PrometheusHandle` на каждый коннект.
|
||||
|
||||
use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
|
||||
use netrunner_logger::{error, info, warn};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
/// Регистрирует глобальный recorder `metrics`-крейта — вызывать ровно один
|
||||
/// раз при старте, до первого `metrics::counter!`/`gauge!`/`histogram!`.
|
||||
pub fn install_recorder() -> PrometheusHandle {
|
||||
PrometheusBuilder::new()
|
||||
.install_recorder()
|
||||
.expect("Failed to install Prometheus recorder")
|
||||
}
|
||||
|
||||
pub async fn run(host: String, port: u16, handle: PrometheusHandle, token: CancellationToken) {
|
||||
let addr = format!("{host}:{port}");
|
||||
let listener = match TcpListener::bind(&addr).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
error!(error = %e, addr = %addr, "Не удалось поднять metrics-листенер");
|
||||
return;
|
||||
}
|
||||
};
|
||||
info!("📊 Metrics-эндпоинт слушает на {}", addr);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = token.cancelled() => {
|
||||
info!("Metrics-listener остановлен по сигналу отмены.");
|
||||
break;
|
||||
}
|
||||
res = listener.accept() => {
|
||||
let Ok((mut stream, _)) = res else { continue };
|
||||
let body = handle.render();
|
||||
tokio::spawn(async move {
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
if let Err(e) = stream.write_all(response.as_bytes()).await {
|
||||
warn!(error = %e, "Metrics-listener: ошибка записи ответа");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,9 @@ impl Network {
|
||||
self.auth.clone(),
|
||||
);
|
||||
|
||||
active_connections.fetch_add(1, Ordering::Relaxed);
|
||||
let active_now = active_connections.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
metrics::gauge!("netrunner_connections_active").set(active_now as f64);
|
||||
metrics::counter!("netrunner_connections_total").increment(1);
|
||||
let conn_counter = active_connections.clone();
|
||||
tokio::spawn(async move {
|
||||
// "Входим" в этот Span. Все логи внутри handler.run() привяжутся к этому IP.
|
||||
@@ -185,7 +187,8 @@ impl Network {
|
||||
if let Err(e) = handler.run().await {
|
||||
error!(error = %e, "⚠️ Server handler terminated with error");
|
||||
}
|
||||
conn_counter.fetch_sub(1, Ordering::Relaxed);
|
||||
let active_now = conn_counter.fetch_sub(1, Ordering::Relaxed) - 1;
|
||||
metrics::gauge!("netrunner_connections_active").set(active_now as f64);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -207,6 +210,9 @@ impl Network {
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
info!("✅ Drain завершён, соединений осталось: {}", active_connections.load(Ordering::Relaxed));
|
||||
info!(
|
||||
"✅ Drain завершён, соединений осталось: {}",
|
||||
active_connections.load(Ordering::Relaxed)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user