From 0b0b132b4b3876e4f846cbeeb3dbd324867de743 Mon Sep 17 00:00:00 2001 From: Michel Heily Date: Wed, 2 Jul 2025 23:47:03 +0300 Subject: [PATCH] metrics-exporter-prometheus: Add API to initialize HTTP listener from TCP listener This commit adds a new API to the `PrometheusBuilder` that allows users to provide their own configured TCP listener. The use case is to enable users to configure the TCP listener before passing it to the exporter - for example, to set socket options or bind to port 0 and retrieve the port number assigned by the OS. Signed-off-by: Michel Heily --- .../src/exporter/builder.rs | 43 ++++++++++++++++--- .../src/exporter/http_listener.rs | 18 ++------ .../src/exporter/mod.rs | 7 ++- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/metrics-exporter-prometheus/src/exporter/builder.rs b/metrics-exporter-prometheus/src/exporter/builder.rs index 33353b46..75b717f6 100644 --- a/metrics-exporter-prometheus/src/exporter/builder.rs +++ b/metrics-exporter-prometheus/src/exporter/builder.rs @@ -105,6 +105,25 @@ impl PrometheusBuilder { self } + /// Same as `with_http_listener`, but allows to bring your own configured tcp listener. + #[cfg(feature = "http-listener")] + #[cfg_attr(docsrs, doc(cfg(feature = "http-listener")))] + #[must_use] + pub fn with_http_listener_from_existing_listener( + mut self, + listener: tokio::net::TcpListener, + ) -> Self { + use std::sync::Arc; + + // We need to wrap the listener in an Arc to allow for cloning. + let listener = Arc::new(listener); + + self.exporter_config = ExporterConfig::HttpListener { + destination: super::ListenDestination::ExistingListener(listener), + }; + self + } + /// Configures the exporter to push periodic requests to a Prometheus [push gateway]. /// /// Running in push gateway mode is mutually exclusive with the HTTP listener i.e. enabling the push gateway will @@ -485,11 +504,25 @@ impl PrometheusBuilder { #[cfg(feature = "http-listener")] ExporterConfig::HttpListener { destination } => match destination { super::ListenDestination::Tcp(listen_address) => { - super::http_listener::new_http_listener( - handle, - listen_address, - allowed_addresses, - )? + let listener = std::net::TcpListener::bind(listen_address) + .and_then(|listener| { + listener.set_nonblocking(true)?; + Ok(listener) + }) + .map_err(|e| BuildError::FailedToCreateHTTPListener(e.to_string()))?; + let listener = tokio::net::TcpListener::from_std(listener).unwrap(); + super::http_listener::new_http_listener(handle, listener, allowed_addresses) + } + super::ListenDestination::ExistingListener(listener) => { + use std::sync::Arc; + + // Should always succeed as we only created the Arc so the TcpListener can be stored in a Clone + let listener = Arc::try_unwrap(listener).map_err(|_| { + BuildError::FailedToCreateHTTPListener( + "Failed to unwrap Arc".to_string(), + ) + })?; + super::http_listener::new_http_listener(handle, listener, allowed_addresses) } #[cfg(feature = "uds-listener")] super::ListenDestination::Uds(listen_path) => { diff --git a/metrics-exporter-prometheus/src/exporter/http_listener.rs b/metrics-exporter-prometheus/src/exporter/http_listener.rs index 0185fa0d..f7362dae 100644 --- a/metrics-exporter-prometheus/src/exporter/http_listener.rs +++ b/metrics-exporter-prometheus/src/exporter/http_listener.rs @@ -1,5 +1,3 @@ -use std::net::SocketAddr; - use http_body_util::Full; use hyper::{ body::{Bytes, Incoming}, @@ -17,7 +15,7 @@ use tokio::net::{TcpListener, TcpStream}; use tokio::net::{UnixListener, UnixStream}; use tracing::warn; -use crate::{common::BuildError, ExporterFuture, PrometheusHandle}; +use crate::{ExporterFuture, PrometheusHandle}; struct HttpListeningExporter { handle: PrometheusHandle, @@ -154,24 +152,16 @@ impl HttpListeningExporter { /// Will return Err if it cannot bind to the listen address pub(crate) fn new_http_listener( handle: PrometheusHandle, - listen_address: SocketAddr, + listener: TcpListener, allowed_addresses: Option>, -) -> Result { - let listener = std::net::TcpListener::bind(listen_address) - .and_then(|listener| { - listener.set_nonblocking(true)?; - Ok(listener) - }) - .map_err(|e| BuildError::FailedToCreateHTTPListener(e.to_string()))?; - let listener = TcpListener::from_std(listener).unwrap(); - +) -> ExporterFuture { let exporter = HttpListeningExporter { handle, allowed_addresses, listener_type: ListenerType::Tcp(listener), }; - Ok(Box::pin(async move { exporter.serve().await.map_err(super::ExporterError::HttpListener) })) + Box::pin(async move { exporter.serve().await.map_err(super::ExporterError::HttpListener) }) } /// Creates an `ExporterFuture` implementing a http listener that serves prometheus metrics. diff --git a/metrics-exporter-prometheus/src/exporter/mod.rs b/metrics-exporter-prometheus/src/exporter/mod.rs index 6c372b51..627331d2 100644 --- a/metrics-exporter-prometheus/src/exporter/mod.rs +++ b/metrics-exporter-prometheus/src/exporter/mod.rs @@ -2,12 +2,14 @@ use http_listener::HttpListeningError; #[cfg(any(feature = "http-listener", feature = "push-gateway"))] use std::future::Future; -#[cfg(feature = "http-listener")] -use std::net::SocketAddr; #[cfg(any(feature = "http-listener", feature = "push-gateway"))] use std::pin::Pin; #[cfg(feature = "push-gateway")] use std::time::Duration; +#[cfg(feature = "http-listener")] +use std::{net::SocketAddr, sync::Arc}; +#[cfg(feature = "http-listener")] +use tokio::net::TcpListener; #[cfg(feature = "push-gateway")] use hyper::Uri; @@ -28,6 +30,7 @@ pub type ExporterFuture = Pin> #[derive(Clone, Debug)] enum ListenDestination { Tcp(SocketAddr), + ExistingListener(Arc), #[cfg(feature = "uds-listener")] Uds(std::path::PathBuf), }