diff --git a/Cargo.lock b/Cargo.lock index 06ea5c6..a704404 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2087,7 +2087,7 @@ dependencies = [ [[package]] name = "telemt" -version = "3.3.15" +version = "3.3.19" dependencies = [ "aes", "anyhow", diff --git a/config.toml b/config.toml index 63fa4ae..f4eb3ae 100644 --- a/config.toml +++ b/config.toml @@ -32,6 +32,7 @@ show = "*" port = 443 # proxy_protocol = false # Enable if behind HAProxy/nginx with PROXY protocol # metrics_port = 9090 +# metrics_listen = "0.0.0.0:9090" # Listen address for metrics (overrides metrics_port) # metrics_whitelist = ["127.0.0.1", "::1", "0.0.0.0/0"] [server.api] diff --git a/src/config/types.rs b/src/config/types.rs index f676f54..7ea1fe7 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -1156,9 +1156,17 @@ pub struct ServerConfig { #[serde(default = "default_proxy_protocol_header_timeout_ms")] pub proxy_protocol_header_timeout_ms: u64, + /// Port for the Prometheus-compatible metrics endpoint. + /// Enables metrics when set; binds on all interfaces (dual-stack) by default. #[serde(default)] pub metrics_port: Option, + /// Listen address for metrics in `IP:PORT` format (e.g. `"127.0.0.1:9090"`). + /// When set, takes precedence over `metrics_port` and binds on the specified address only. + #[serde(default)] + pub metrics_listen: Option, + + /// CIDR whitelist for the metrics endpoint. #[serde(default = "default_metrics_whitelist")] pub metrics_whitelist: Vec, @@ -1186,6 +1194,7 @@ impl Default for ServerConfig { proxy_protocol: false, proxy_protocol_header_timeout_ms: default_proxy_protocol_header_timeout_ms(), metrics_port: None, + metrics_listen: None, metrics_whitelist: default_metrics_whitelist(), api: ApiConfig::default(), listeners: Vec::new(), diff --git a/src/maestro/runtime_tasks.rs b/src/maestro/runtime_tasks.rs index 329e267..d9691a8 100644 --- a/src/maestro/runtime_tasks.rs +++ b/src/maestro/runtime_tasks.rs @@ -279,11 +279,32 @@ pub(crate) async fn spawn_metrics_if_configured( ip_tracker: Arc, config_rx: watch::Receiver>, ) { - if let Some(port) = config.server.metrics_port { + // metrics_listen takes precedence; fall back to metrics_port for backward compat. + let metrics_target: Option<(u16, Option)> = + if let Some(ref listen) = config.server.metrics_listen { + match listen.parse::() { + Ok(addr) => Some((addr.port(), Some(listen.clone()))), + Err(e) => { + startup_tracker + .skip_component( + COMPONENT_METRICS_START, + Some(format!("invalid metrics_listen \"{}\": {}", listen, e)), + ) + .await; + None + } + } + } else { + config.server.metrics_port.map(|p| (p, None)) + }; + + if let Some((port, listen)) = metrics_target { + let fallback_label = format!("port {}", port); + let label = listen.as_deref().unwrap_or(&fallback_label); startup_tracker .start_component( COMPONENT_METRICS_START, - Some(format!("spawn metrics endpoint on {}", port)), + Some(format!("spawn metrics endpoint on {}", label)), ) .await; let stats = stats.clone(); @@ -294,6 +315,7 @@ pub(crate) async fn spawn_metrics_if_configured( tokio::spawn(async move { metrics::serve( port, + listen, stats, beobachten, ip_tracker_metrics, @@ -308,7 +330,7 @@ pub(crate) async fn spawn_metrics_if_configured( Some("metrics task spawned".to_string()), ) .await; - } else { + } else if config.server.metrics_listen.is_none() { startup_tracker .skip_component( COMPONENT_METRICS_START, diff --git a/src/metrics.rs b/src/metrics.rs index 02edfd7..f4f8a2e 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -21,6 +21,7 @@ use crate::transport::{ListenOptions, create_listener}; pub async fn serve( port: u16, + listen: Option, stats: Arc, beobachten: Arc, ip_tracker: Arc, @@ -28,6 +29,33 @@ pub async fn serve( whitelist: Vec, ) { let whitelist = Arc::new(whitelist); + + // If `metrics_listen` is set, bind on that single address only. + if let Some(ref listen_addr) = listen { + let addr: SocketAddr = match listen_addr.parse() { + Ok(a) => a, + Err(e) => { + warn!(error = %e, "Invalid metrics_listen address: {}", listen_addr); + return; + } + }; + let is_ipv6 = addr.is_ipv6(); + match bind_metrics_listener(addr, is_ipv6) { + Ok(listener) => { + info!("Metrics endpoint: http://{}/metrics and /beobachten", addr); + serve_listener( + listener, stats, beobachten, ip_tracker, config_rx, whitelist, + ) + .await; + } + Err(e) => { + warn!(error = %e, "Failed to bind metrics on {}", addr); + } + } + return; + } + + // Fallback: bind on 0.0.0.0 and [::] using metrics_port. let mut listener_v4 = None; let mut listener_v6 = None;