This repository was archived by the owner on Jan 16, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 212
feat(cli): support dynamic port assignment for metrics server #3240
Open
teddyknox
wants to merge
2
commits into
main
Choose a base branch
from
teddyknox/add-dynamic-port-support-to-metrics-server
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,37 +1,104 @@ | ||||||
| //! Utilities for spinning up a prometheus metrics server. | ||||||
| //! | ||||||
| //! # Design | ||||||
| //! | ||||||
| //! We manually serve metrics via `hyper` rather than using the built-in HTTP listener from | ||||||
| //! `metrics-exporter-prometheus`. This is because [`PrometheusBuilder::with_http_listener`] | ||||||
| //! doesn't expose the actual bound address—it only accepts a [`SocketAddr`] and returns | ||||||
| //! `Result<(), BuildError>`. When port 0 is used (letting the OS assign an available port), | ||||||
| //! there's no way to discover what port was actually assigned. | ||||||
| //! | ||||||
| //! We considered pre-binding a `TcpListener` to get the port, dropping it, then passing that | ||||||
| //! port to the builder—but this has a race condition where another process could grab the port | ||||||
| //! in between. | ||||||
| //! | ||||||
| //! Instead, we use [`PrometheusBuilder::build_recorder`] to create the recorder without an HTTP | ||||||
| //! server, bind our own [`TcpListener`], and serve metrics manually. This approach: | ||||||
| //! - Eliminates the race condition (the same listener is used throughout) | ||||||
| //! - Allows returning the actual bound address to callers | ||||||
| //! - Follows the same pattern used by [reth's metrics infrastructure][reth-metrics] | ||||||
| //! | ||||||
| //! [reth-metrics]: https://github.com/paradigmxyz/reth/blob/main/crates/node/metrics/src/server.rs | ||||||
|
|
||||||
| use metrics_exporter_prometheus::{BuildError, PrometheusBuilder}; | ||||||
| use crate::PrometheusError; | ||||||
| use http::{Response, header::CONTENT_TYPE}; | ||||||
| use http_body_util::Full; | ||||||
| use hyper::{body::Bytes, server::conn::http1, service::service_fn}; | ||||||
| use hyper_util::rt::TokioIo; | ||||||
| use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle}; | ||||||
| use metrics_process::Collector; | ||||||
| use std::{ | ||||||
| net::{IpAddr, SocketAddr}, | ||||||
| thread::{self, sleep}, | ||||||
| time::Duration, | ||||||
| }; | ||||||
| use tracing::info; | ||||||
| use std::{convert::Infallible, net::SocketAddr, sync::Arc, time::Duration}; | ||||||
| use tokio::net::TcpListener; | ||||||
| use tracing::{error, info}; | ||||||
|
|
||||||
| /// Start a Prometheus metrics server on the given port. | ||||||
| pub fn init_prometheus_server(addr: IpAddr, metrics_port: u16) -> Result<(), BuildError> { | ||||||
| let prometheus_addr = SocketAddr::from((addr, metrics_port)); | ||||||
| let builder = PrometheusBuilder::new().with_http_listener(prometheus_addr); | ||||||
| /// Start a Prometheus metrics server on the given address. | ||||||
| /// | ||||||
| /// If the port is 0, the OS will assign an available port. | ||||||
| /// Returns the actual address the server is bound to. | ||||||
| /// | ||||||
| /// This function must be called from within a tokio runtime. | ||||||
| pub async fn init_prometheus_server(addr: SocketAddr) -> Result<SocketAddr, PrometheusError> { | ||||||
| let listener = TcpListener::bind(addr).await?; | ||||||
| let actual_addr = listener.local_addr()?; | ||||||
|
|
||||||
| builder.install()?; | ||||||
| // Build recorder without HTTP server - we serve it ourselves | ||||||
| let recorder = PrometheusBuilder::new().build_recorder(); | ||||||
| let handle = Arc::new(recorder.handle()); | ||||||
|
|
||||||
| // Initialise collector for system metrics e.g. CPU, memory, etc. | ||||||
| let collector = Collector::default(); | ||||||
| collector.describe(); | ||||||
| // Set as global recorder | ||||||
| metrics::set_global_recorder(recorder)?; | ||||||
|
|
||||||
| thread::spawn(move || { | ||||||
| // Spawn task for periodic system metrics collection | ||||||
| tokio::spawn(async move { | ||||||
| let collector = Collector::default(); | ||||||
| collector.describe(); | ||||||
| loop { | ||||||
| collector.collect(); | ||||||
| sleep(Duration::from_secs(60)); | ||||||
| tokio::time::sleep(Duration::from_secs(60)).await; | ||||||
| } | ||||||
| }); | ||||||
|
|
||||||
| // Spawn task to serve metrics endpoint | ||||||
| tokio::spawn(serve_metrics(listener, handle)); | ||||||
|
|
||||||
| info!( | ||||||
| target: "prometheus", | ||||||
| "Serving metrics at: http://{}", | ||||||
| prometheus_addr | ||||||
| actual_addr | ||||||
| ); | ||||||
|
|
||||||
| Ok(()) | ||||||
| Ok(actual_addr) | ||||||
| } | ||||||
|
|
||||||
| async fn serve_metrics(listener: TcpListener, handle: Arc<PrometheusHandle>) { | ||||||
| loop { | ||||||
| let (stream, _) = match listener.accept().await { | ||||||
| Ok(conn) => conn, | ||||||
| Err(e) => { | ||||||
| error!(target: "prometheus", "failed to accept connection: {}", e); | ||||||
| continue; | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| let handle = Arc::clone(&handle); | ||||||
| tokio::spawn(async move { | ||||||
| let service = service_fn(move |_req| { | ||||||
| let metrics = handle.render(); | ||||||
| async move { | ||||||
| let response = Response::builder() | ||||||
| .header(CONTENT_TYPE, "text/plain; charset=utf-8") | ||||||
| .body(Full::new(Bytes::from(metrics))) | ||||||
| .unwrap(); | ||||||
|
||||||
| .unwrap(); | |
| .expect("failed to build HTTP response for Prometheus metrics"); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding hyper and hyper-util to the workspace dependencies in the root Cargo.toml. This would ensure consistent versions across the workspace and make it easier to manage updates. Currently, these dependencies are specified with explicit versions only in this crate, while hyper is already transitively used in the project (version 1.8.1). Using workspace dependencies would follow the pattern used for other shared dependencies like tokio, http, and http-body-util.