#![cfg(feature = "prometheus")]
use crate::{default_from_clap, shutdown::await_shutdown};
use clap::Parser;
use eyre::{bail, ensure, Result as EyreResult, WrapErr as _};
use hyper::{
body::HttpBody,
header::CONTENT_TYPE,
service::{make_service_fn, service_fn},
Body, Method, Request, Response, Server,
};
use once_cell::sync::Lazy;
use prometheus::{
opts, register_counter, register_gauge, register_histogram, Counter, Encoder as _, Gauge,
Histogram,
};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use tracing::{error, info, instrument, trace};
use url::{Host, Url};
#[cfg(feature = "otlp")]
use crate::trace_from_headers;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Parser)]
#[group(skip)]
pub struct Options {
#[clap(long, env, default_value = "http://127.0.0.1:9998/metrics")]
pub prometheus: Url,
}
default_from_clap!(Options);
static REQ_COUNTER: Lazy<Counter> = Lazy::new(|| {
register_counter!(opts!(
"prometheus_requests_total",
"Number of Prometheus scrape requests made."
))
.unwrap()
});
static REQ_BODY_GAUGE: Lazy<Gauge> = Lazy::new(|| {
register_gauge!(opts!(
"prometheus_response_size_bytes",
"The Prometheus response sizes in bytes."
))
.unwrap()
});
static REQ_HISTOGRAM: Lazy<Histogram> = Lazy::new(|| {
register_histogram!(
"prometheus_request_duration_seconds",
"The Prometheus request latencies in seconds."
)
.unwrap()
});
#[allow(clippy::unnecessary_wraps)]
#[allow(clippy::needless_pass_by_value)]
#[allow(clippy::unused_async)]
async fn serve_req(_req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
let encoder = prometheus::TextEncoder;
let metric_families = prometheus::gather();
let mut buffer = vec![];
let result = encoder.encode(&metric_families, &mut buffer);
if let Err(e) = result {
error!("Internal server error: {}", e);
let response = Response::builder()
.status(500)
.body(Body::from(e.to_string()))
.unwrap(); return Ok(response);
}
let response = Response::builder()
.status(200)
.header(CONTENT_TYPE, encoder.format_type())
.body(Body::from(buffer))
.unwrap();
Ok(response)
}
#[allow(clippy::unused_async)] #[instrument(level="debug", name="prometheus_request", skip(req), fields(http.uri = %req.uri(), http.method = %req.method()))]
async fn route(req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
#[cfg(feature = "otlp")]
trace_from_headers(req.headers());
trace!("Receiving request at path {}", req.uri());
REQ_COUNTER.inc();
let timer = REQ_HISTOGRAM.start_timer();
let response = match (req.method(), req.uri().path()) {
(&Method::GET, "/metrics") => serve_req(req).await?,
_ => Response::builder()
.status(404)
.body(Body::from("404"))
.unwrap(),
};
#[allow(clippy::cast_precision_loss)]
REQ_BODY_GAUGE.set(response.body().size_hint().lower() as f64);
timer.observe_duration();
Ok(response)
}
pub async fn main(options: Options) -> EyreResult<()> {
ensure!(
options.prometheus.scheme() == "http",
"Only http:// is supported in {}",
options.prometheus
);
ensure!(
options.prometheus.path() == "/metrics",
"Only /metrics is supported in {}",
options.prometheus
);
let ip: IpAddr = match options.prometheus.host() {
Some(Host::Ipv4(ip)) => ip.into(),
Some(Host::Ipv6(ip)) => ip.into(),
Some(_) => bail!("Cannot bind {}", options.prometheus),
None => Ipv4Addr::LOCALHOST.into(),
};
let port = options.prometheus.port().unwrap_or(9998);
let addr = SocketAddr::new(ip, port);
let server = Server::try_bind(&addr)
.wrap_err("Could not bind Prometheus server port")?
.serve(make_service_fn(|_| async {
Ok::<_, hyper::Error>(service_fn(route))
}))
.with_graceful_shutdown(await_shutdown());
info!(url = %options.prometheus, "Metrics server listening");
server.await?;
Ok(())
}