Exporting Prometheus metrics with Axum

Ellie Huxtable

Axum으로 Prometheus 메트릭 내보내기

가시성(Observability)은 정말 중요해요! 저는 보통 Rust에서 HTTP 프레임워크로 Rust의 Axum을 사용해요. 꽤 쓰기 편하고 빠르거든요.

tower-http는 여러 프로젝트에서 쓰이는 유용한 HTTP 미들웨어들을 제공해요. 현재는 메트릭 미들웨어를 제공하지 않지만, 언젠가는 제공하게 될지도 몰라요!

이슈에서 추적할 수 있어요

이 과정을 자동으로 처리해주는 크레이트도 꽤 많지만, Axum 예제에서는 metrics를 사용하라고 권장하고 있어요. 솔직히 저는 그렇게 복잡한 건 필요하지 않고, 대부분 그냥 카운터 같은 게 포함된 /metrics 엔드포인트만 있으면 충분해서 — 그래서 metrics를 쓰기로 했어요!

어쨌든 먼저 prometheus exporter를 설정해야 해요. 이게 바로 /metrics의 내용을 생성하는 부분이에요. metrics-exporter-prometheus 크레이트를 사용해요. 전역 recorder는 실행 파일에서만 설정하는 게 좋아요. 라이브러리라면 이 부분은 사용자에게 맡기면 돼요.

이 코드는 위에 링크한 Axum 예제에서 거의 그대로 가져왔어요 😇

fn setup_metrics_recorder() -> PrometheusHandle {
    const EXPONENTIAL_SECONDS: &[f64] = &[
        0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
    ];

    PrometheusBuilder::new()
        .set_buckets_for_metric(
            Matcher::Full("http_requests_duration_seconds".to_string()),
            EXPONENTIAL_SECONDS,
        )
        .unwrap()
        .install_recorder()
        .unwrap()
}

사실 오늘은 두 개의 메트릭을 설정하는데, 그중 history_requests_duration_seconds만 별도 설정이 필요해요. 히스토그램이라 exporter에게 데이터를 어떻게 버킷으로 나눌지 알려줘야 하기 때문이에요.

이 설정이 끝나면 Axum 미들웨어를 작성할 수 있어요! (예제에서 가져와 제대로 컴파일되도록 수정했어요. PR을 올려둘게요)

/// Middleware to record some common HTTP metrics
/// Generic over B to allow for arbitrary body types (eg Vec<u8>, Streams, a deserialized thing, etc)
/// Someday tower-http might provide a metrics middleware: https://github.com/tower-rs/tower-http/issues/57
pub async fn track_metrics<B>(req: Request<B>, next: Next<B>) -> impl IntoResponse {
    let start = Instant::now();

    let path = if let Some(matched_path) = req.extensions().get::<MatchedPath>() {
        matched_path.as_str().to_owned()
    } else {
        req.uri().path().to_owned()
    };

    let method = req.method().clone();

    // Run the rest of the request handling first, so we can measure it and get response
    // codes.
    let response = next.run(req).await;

    let latency = start.elapsed().as_secs_f64();
    let status = response.status().as_u16().to_string();

    let labels = [
        ("method", method.to_string()),
        ("path", path),
        ("status", status),
    ];

    metrics::increment_counter!("http_requests_total", &labels);
    metrics::histogram!("http_requests_duration_seconds", latency, &labels);

    response
}

그런 다음 Axum 라우터를 설정하는 곳에서 /metrics 라우트를 연결해주면 돼요! 외부에 공개되지 않도록 꼭 확인해 주세요.

여기서 std::future::ready에 대해 알게 됐어요! 이 함수는 값을 담아 즉시 사용할 수 있는 future를 만들어 줘요. 예를 들면:

let f = std::future::ready(1);
assert_eq!(a.await, 1);
let recorder_handle = setup_metrics_recorder();

let router = Router::new()
	.route("/metrics", get(move || ready(recorder_handle.render())))
	.layer(axum::middleware::from_fn(track_metrics));

정말 이게 다예요!

원문은 Ellie Huxtable님이 에 게재했습니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.