tide_disco/
metrics.rs

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