Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get default metrics working with axum #8

Open
clux opened this issue Nov 1, 2021 · 1 comment
Open

Get default metrics working with axum #8

clux opened this issue Nov 1, 2021 · 1 comment

Comments

@clux
Copy link
Member

clux commented Nov 1, 2021

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.

@davidpdrsn
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants