version-rs
version-rs copied to clipboard
Get default metrics working with axum
We previously used actix-web-prom to give us good baseline metrics:
$ curl 0.0.0.0:8000/metrics
api_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="0.005"} 11
...
...
api_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="+Inf"} 11
api_http_requests_duration_seconds_sum{endpoint="/",method="GET",status="200"} 0.001559851
api_http_requests_duration_seconds_count{endpoint="/",method="GET",status="200"} 11
# HELP api_http_requests_total Total number of HTTP requests
# TYPE api_http_requests_total counter
api_http_requests_total{endpoint="/",method="GET",status="200"} 11
but this had a lot of upgrade issues through the whole "required actix beta".
So, after the axum port in #6 we should see if there's a way to get good baseline default metrics added. Probably requires a bit of a wait for metrics ecosystem to evolve though.
Metrics in general is something I want to have built in to tower-http. Haven't quite found the right design yet though.
If you wanna roll your own this does it (using metrics):
use axum::{
extract::Extension,
handler::get,
http::{Request, Response},
response::IntoResponse,
AddExtensionLayer, Router,
};
use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle};
use pin_project_lite::pin_project;
use std::{
convert::Infallible,
future::Future,
net::SocketAddr,
pin::Pin,
task::{Context, Poll},
time::{Duration, Instant},
};
use tower::{Service, ServiceBuilder};
#[tokio::main]
async fn main() {
let recorder = PrometheusBuilder::new()
.set_buckets_for_metric(
Matcher::Full("api_http_requests_duration_seconds".to_string()),
&[
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
],
)
.build();
let recorder_handle = recorder.handle();
metrics::set_boxed_recorder(Box::new(recorder)).expect("failed to set metrics recorder");
let app = Router::new()
.route("/", get(handler))
.route("/metrics", get(metrics_handler))
.layer(
ServiceBuilder::new()
.layer_fn(|inner| RecordMetrics { inner })
.layer(AddExtensionLayer::new(recorder_handle)),
);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn handler() -> impl IntoResponse {
// simulate slowness
tokio::time::sleep(Duration::from_millis(100)).await;
}
async fn metrics_handler(Extension(handle): Extension<PrometheusHandle>) -> impl IntoResponse {
handle.render()
}
#[derive(Clone)]
struct RecordMetrics<S> {
inner: S,
}
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for RecordMetrics<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>, Error = Infallible>,
{
type Response = S::Response;
type Error = Infallible;
type Future = RecordMetricsFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let start = Instant::now();
let path = req.uri().path().to_string();
RecordMetricsFuture {
inner: self.inner.call(req),
path: Some(path),
start,
}
}
}
pin_project! {
struct RecordMetricsFuture<F> {
#[pin]
inner: F,
path: Option<String>,
start: Instant,
}
}
impl<F, B> Future for RecordMetricsFuture<F>
where
F: Future<Output = Result<Response<B>, Infallible>>,
{
type Output = Result<Response<B>, Infallible>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this.inner.poll(cx) {
Poll::Ready(Ok(res)) => {
let latency = this.start.elapsed().as_secs_f64();
let status = res.status().as_u16().to_string();
let path = this.path.take().expect("future polled after completion");
let labels = [("path", path), ("status", status)];
metrics::increment_counter!("api_http_requests_total", &labels);
metrics::histogram!("api_http_requests_duration_seconds", latency, &labels);
Poll::Ready(Ok(res))
}
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
}
This results in these metrics:
❯ curl localhost:3000/metrics
# TYPE api_http_requests_total counter
api_http_requests_total{status="200",path="/"} 1
# TYPE api_http_requests_duration_seconds histogram
api_http_requests_duration_seconds_bucket{status="200",path="/",le="0.005"} 0
api_http_requests_duration_seconds_bucket{status="200",path="/",le="0.01"} 0
api_http_requests_duration_seconds_bucket{status="200",path="/",le="0.025"} 0
api_http_requests_duration_seconds_bucket{status="200",path="/",le="0.05"} 0
api_http_requests_duration_seconds_bucket{status="200",path="/",le="0.1"} 0
api_http_requests_duration_seconds_bucket{status="200",path="/",le="0.25"} 1
api_http_requests_duration_seconds_bucket{status="200",path="/",le="0.5"} 1
api_http_requests_duration_seconds_bucket{status="200",path="/",le="1"} 1
api_http_requests_duration_seconds_bucket{status="200",path="/",le="2.5"} 1
api_http_requests_duration_seconds_bucket{status="200",path="/",le="5"} 1
api_http_requests_duration_seconds_bucket{status="200",path="/",le="10"} 1
api_http_requests_duration_seconds_bucket{status="200",path="/",le="+Inf"} 1
api_http_requests_duration_seconds_sum{status="200",path="/"} 0.106305708
api_http_requests_duration_seconds_count{status="200",path="/"} 1