use std::convert::Infallible;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Instant;
use axum::http::{Method, Request, Response};
use axum::response::IntoResponse;
use axum::{routing::get, Router};
use futures::future::BoxFuture;
use hwhkit_buildinfo::build_info;
use hwhkit_config::MetricsConfig;
use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
use tower::{Layer, Service};
#[derive(Clone)]
pub struct MetricsState {
pub handle: PrometheusHandle,
}
pub fn install_recorder() -> Result<PrometheusHandle, String> {
let builder = PrometheusBuilder::new();
let handle = builder
.install_recorder()
.map_err(|e| format!("failed to install prometheus recorder: {e}"))?;
let info = build_info!();
metrics::gauge!(
"hwhkit_build_info",
"git_sha" => info.git_sha,
"rust_version" => info.rust_version,
"version" => info.cargo_version
)
.set(1.0);
Ok(handle)
}
async fn metrics_handler(state: axum::extract::State<Arc<MetricsState>>) -> impl IntoResponse {
let body = state.handle.render();
(
[(
axum::http::header::CONTENT_TYPE,
"text/plain; version=0.0.4",
)],
body,
)
}
pub fn router(cfg: &MetricsConfig, handle: PrometheusHandle) -> Router {
let state = Arc::new(MetricsState { handle });
Router::new()
.route(&cfg.path, get(metrics_handler))
.with_state(state)
}
#[derive(Clone, Default)]
pub struct HttpMetricsLayer;
impl HttpMetricsLayer {
pub fn new() -> Self {
Self
}
}
impl<S> Layer<S> for HttpMetricsLayer {
type Service = HttpMetrics<S>;
fn layer(&self, inner: S) -> Self::Service {
HttpMetrics { inner }
}
}
#[derive(Clone)]
pub struct HttpMetrics<S> {
inner: S,
}
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for HttpMetrics<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>, Error = Infallible>
+ Clone
+ Send
+ 'static,
S::Future: Send + 'static,
ReqBody: Send + 'static,
ResBody: Send + 'static,
{
type Response = Response<ResBody>;
type Error = Infallible;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
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 method_label: &'static str = method_to_static(req.method());
let path_owned: String = req
.extensions()
.get::<axum::extract::MatchedPath>()
.map(|m| m.as_str().to_owned())
.unwrap_or_else(|| String::from("unmatched"));
let started = Instant::now();
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);
Box::pin(async move {
let response = inner.call(req).await?;
let status_u16 = response.status().as_u16();
let mut status_buf = itoa::Buffer::new();
let status_owned: String = status_buf.format(status_u16).to_owned();
let elapsed = started.elapsed().as_secs_f64();
metrics::counter!(
"http_requests_total",
"method" => method_label,
"path" => path_owned.clone(),
"status" => status_owned.clone(),
)
.increment(1);
metrics::histogram!(
"http_request_duration_seconds",
"method" => method_label,
"path" => path_owned,
"status" => status_owned,
)
.record(elapsed);
Ok(response)
})
}
}
fn method_to_static(m: &Method) -> &'static str {
match m.as_str() {
"GET" => "GET",
"POST" => "POST",
"PUT" => "PUT",
"PATCH" => "PATCH",
"DELETE" => "DELETE",
"HEAD" => "HEAD",
"OPTIONS" => "OPTIONS",
"TRACE" => "TRACE",
"CONNECT" => "CONNECT",
_ => "OTHER",
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::routing::get;
use axum::Router;
use tower::{Service, ServiceExt};
#[tokio::test]
async fn http_metrics_layer_handles_repeated_calls() {
let app: Router = Router::new()
.route("/ping", get(|| async { "pong" }))
.layer(HttpMetricsLayer::new());
for _ in 0..5 {
let mut svc = app.clone();
let response = ServiceExt::<Request<Body>>::ready(&mut svc)
.await
.expect("ready")
.call(Request::builder().uri("/ping").body(Body::empty()).unwrap())
.await
.expect("call");
assert_eq!(response.status(), StatusCode::OK);
}
}
}