version-rs icon indicating copy to clipboard operation
version-rs copied to clipboard

Get default metrics working with axum

Open clux opened this issue 4 years ago • 1 comments

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.

clux avatar Nov 01 '21 14:11 clux

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

davidpdrsn avatar Nov 02 '21 14:11 davidpdrsn