1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
// Copyright (c) 2022 Espresso Systems (espressosys.com)
// This file is part of the tide-disco library.
// You should have received a copy of the MIT License
// along with the tide-disco library. If not, see <https://mit-license.org/>.
//! Support for routes using the Prometheus metrics format.
use crate::{
method::ReadState,
request::RequestParams,
route::{self, RouteError},
};
use async_trait::async_trait;
use derive_more::From;
use futures::future::{BoxFuture, FutureExt};
use prometheus::{Encoder, TextEncoder};
use std::{borrow::Cow, error::Error, fmt::Debug};
pub trait Metrics {
type Error: Debug + Error;
fn export(&self) -> Result<String, Self::Error>;
}
impl Metrics for prometheus::Registry {
type Error = prometheus::Error;
fn export(&self) -> Result<String, Self::Error> {
let mut buffer = vec![];
let encoder = TextEncoder::new();
let metric_families = self.gather();
encoder.encode(&metric_families, &mut buffer)?;
String::from_utf8(buffer).map_err(|err| {
prometheus::Error::Msg(format!(
"could not convert Prometheus output to UTF-8: {err}",
))
})
}
}
/// A [Handler](route::Handler) which delegates to an async function returning metrics.
///
/// The function type `F` should be callable as
/// `async fn(RequestParams, &State) -> Result<&R, Error>`. The [Handler] implementation will
/// automatically convert the result `R` to a [tide::Response] by exporting the [Metrics] to text,
/// or the error `Error` to a [RouteError] using [RouteError::AppSpecific].
///
/// # Limitations
///
/// [Like many function parameters](crate#boxed-futures) in [tide_disco](crate), the handler
/// function is required to return a [BoxFuture].
#[derive(From)]
pub(crate) struct Handler<F>(F);
#[async_trait]
impl<F, T, State, Error> route::Handler<State, Error> for Handler<F>
where
F: 'static + Send + Sync + Fn(RequestParams, &State::State) -> BoxFuture<Result<Cow<T>, Error>>,
T: 'static + Clone + Metrics,
State: 'static + Send + Sync + ReadState,
Error: 'static,
{
async fn handle(
&self,
req: RequestParams,
state: &State,
) -> Result<tide::Response, RouteError<Error>> {
let exported = state
.read(|state| {
let fut = (self.0)(req, state);
async move {
let metrics = fut.await.map_err(RouteError::AppSpecific)?;
metrics
.export()
.map_err(|err| RouteError::ExportMetrics(err.to_string()))
}
.boxed()
})
.await?;
Ok(exported.into())
}
}