axum_prometheus/
builder.rs

1use std::borrow::Cow;
2use std::marker::PhantomData;
3
4#[cfg(feature = "prometheus")]
5use metrics_exporter_prometheus::PrometheusHandle;
6
7use crate::{set_prefix, GenericMetricLayer, MakeDefaultHandle, Traffic};
8
9#[doc(hidden)]
10mod sealed {
11    use super::{LayerOnly, Paired};
12    pub trait Sealed {}
13    impl Sealed for LayerOnly {}
14    impl Sealed for Paired {}
15}
16pub trait MetricBuilderState: sealed::Sealed {}
17
18pub enum Paired {}
19pub enum LayerOnly {}
20impl MetricBuilderState for Paired {}
21impl MetricBuilderState for LayerOnly {}
22
23#[derive(Default, Clone)]
24/// Determines how endpoints are reported.
25pub enum EndpointLabel {
26    /// The reported endpoint label is always the fully qualified uri path that has been requested.
27    Exact,
28    /// The reported endpoint label is determined by first trying to extract and return [`axum::extract::MatchedPath`],
29    /// and if that fails (typically on [nested routes]) it falls back to [`EndpointLabel::Exact`] behavior. This is
30    /// the default option.
31    ///
32    /// [nested routes]: https://docs.rs/axum/latest/axum/extract/struct.MatchedPath.html#matched-path-in-nested-routers
33    #[default]
34    MatchedPath,
35    /// Same as [`EndpointLabel::MatchedPath`], but instead of falling back to the exact uri called, it's given to a user-defined
36    /// fallback function, that is expected to produce a String, which is then reported to Prometheus.
37    MatchedPathWithFallbackFn(for<'f> fn(&'f str) -> String),
38}
39
40/// A builder for [`GenericMetricLayer`] that enables further customizations.
41///
42/// Most of the example code uses [`PrometheusMetricLayerBuilder`], which is only a type alias
43/// specialized for Prometheus.
44///
45/// ## Example
46/// ```rust,no_run
47/// use axum_prometheus::PrometheusMetricLayerBuilder;
48///
49/// let (metric_layer, metric_handle) = PrometheusMetricLayerBuilder::new()
50///     .with_ignore_patterns(&["/metrics", "/sensitive"])
51///     .with_group_patterns_as("/foo", &["/foo/:bar", "/foo/:bar/:baz"])
52///     .with_group_patterns_as("/bar", &["/auth/*path"])
53///     .with_default_metrics()
54///     .build_pair();
55/// ```
56#[derive(Clone, Default)]
57pub struct MetricLayerBuilder<'a, T, M, S: MetricBuilderState> {
58    pub(crate) traffic: Traffic<'a>,
59    pub(crate) metric_handle: Option<T>,
60    pub(crate) metric_prefix: Option<String>,
61    pub(crate) enable_body_size: bool,
62    pub(crate) no_initialize_metrics: bool,
63    pub(crate) _marker: PhantomData<(S, M)>,
64}
65
66impl<'a, T, M, S> MetricLayerBuilder<'a, T, M, S>
67where
68    S: MetricBuilderState,
69{
70    /// Skip reporting a specific route pattern.
71    ///
72    /// In the following example
73    /// ```rust
74    /// use axum_prometheus::PrometheusMetricLayerBuilder;
75    ///
76    /// let metric_layer = PrometheusMetricLayerBuilder::new()
77    ///     .with_ignore_pattern("/metrics")
78    ///     .build();
79    /// ```
80    /// any request that's URI path matches "/metrics" will be skipped altogether
81    /// when reporting to the external provider.
82    ///
83    /// Supports the same features as `axum`'s Router.
84    ///
85    /// _Note: `with_ignore_pattern` and `with_allow_pattern` are mutually exclusive. If you call both, the last one called takes precedence and resets the previous patterns._
86    ///
87    /// _Note that ignore patterns are always checked before any other group pattern rule is applied
88    /// and it short-circuits if a certain route is ignored._
89    pub fn with_ignore_pattern(mut self, ignore_pattern: &'a str) -> Self {
90        self.traffic.with_ignore_pattern(ignore_pattern);
91        self
92    }
93
94    /// Skip reporting a collection of route patterns.
95    ///
96    /// Equivalent with calling [`with_ignore_pattern`] repeatedly.
97    ///
98    /// ```rust
99    /// use axum_prometheus::PrometheusMetricLayerBuilder;
100    ///
101    /// let metric_layer = PrometheusMetricLayerBuilder::new()
102    ///     .with_ignore_patterns(&["/foo", "/bar/:baz"])
103    ///     .build();
104    /// ```
105    ///
106    /// Supports the same features as `axum`'s Router.
107    ///
108    /// _Note: `with_ignore_patterns` and `with_allow_patterns` are mutually exclusive. If you call both, the last one called takes precedence and resets the previous patterns._
109    ///
110    /// _Note that ignore patterns are always checked before any other group pattern rule is applied
111    /// and it short-circuits if a certain route is ignored._
112    ///
113    /// [`with_ignore_pattern`]: crate::MetricLayerBuilder::with_ignore_pattern
114    pub fn with_ignore_patterns(mut self, ignore_patterns: &'a [&'a str]) -> Self {
115        self.traffic.with_ignore_patterns(ignore_patterns);
116        self
117    }
118
119    /// Only report requests matching a specific route pattern.
120    ///
121    /// In the following example
122    /// ```rust
123    /// use axum_prometheus::PrometheusMetricLayerBuilder;
124    ///
125    /// let metric_layer = PrometheusMetricLayerBuilder::new()
126    ///     .with_allow_pattern("/api/*path")
127    ///     .build();
128    /// ```
129    /// only requests whose URI path matches "/api/*path" will be reported.
130    ///
131    /// Supports the same features as `axum`'s Router.
132    ///
133    /// _Note: `with_allow_pattern` and `with_ignore_pattern` are mutually exclusive. If you call both, the last one called takes precedence and resets the previous patterns._
134    pub fn with_allow_pattern(mut self, allow_pattern: &'a str) -> Self {
135        self.traffic.with_allow_pattern(allow_pattern);
136        self
137    }
138
139    /// Only report requests matching a collection of route patterns.
140    ///
141    /// Equivalent with calling [`with_allow_pattern`] repeatedly.
142    ///
143    /// ```rust
144    /// use axum_prometheus::PrometheusMetricLayerBuilder;
145    ///
146    /// let metric_layer = PrometheusMetricLayerBuilder::new()
147    ///     .with_allow_patterns(&["/api/*path", "/public/*path"])
148    ///     .build();
149    /// ```
150    ///
151    /// Supports the same features as `axum`'s Router.
152    ///
153    /// _Note: `with_allow_patterns` and `with_ignore_patterns` are mutually exclusive. If you call both, the last one called takes precedence and resets the previous patterns._
154    ///
155    /// [`with_allow_pattern`]: crate::MetricLayerBuilder::with_allow_pattern
156    pub fn with_allow_patterns(mut self, allow_patterns: &'a [&'a str]) -> Self {
157        self.traffic.with_allow_patterns(allow_patterns);
158        self
159    }
160
161    /// Group matching route patterns and report them under the given (arbitrary) endpoint.
162    ///
163    /// This feature is commonly useful for parametrized routes. Let's say you have these two routes:
164    ///  - `/foo/:bar`
165    ///  - `/foo/:bar/:baz`
166    ///
167    /// By default every unique request URL path gets reported with different endpoint label.
168    /// This feature allows you to report these under a custom endpoint, for instance `/foo`:
169    ///
170    /// ```rust
171    /// use axum_prometheus::PrometheusMetricLayerBuilder;
172    ///
173    /// let metric_layer = PrometheusMetricLayerBuilder::new()
174    ///     // the choice of "/foo" is arbitrary
175    ///     .with_group_patterns_as("/foo", &["/foo/:bar", "foo/:bar/:baz"])
176    ///     .build();
177    /// ```
178    pub fn with_group_patterns_as(
179        mut self,
180        group_pattern: &'a str,
181        patterns: &'a [&'a str],
182    ) -> Self {
183        self.traffic.with_group_patterns_as(group_pattern, patterns);
184        self
185    }
186
187    /// Determine how endpoints are reported. For more information, see [`EndpointLabel`].
188    ///
189    /// [`EndpointLabel`]: crate::EndpointLabel
190    pub fn with_endpoint_label_type(mut self, endpoint_label: EndpointLabel) -> Self {
191        self.traffic.with_endpoint_label_type(endpoint_label);
192        self
193    }
194
195    /// Enable response body size tracking.
196    ///
197    /// #### Note:
198    /// This may introduce some performance overhead.
199    pub fn enable_response_body_size(mut self, enable: bool) -> Self {
200        self.enable_body_size = enable;
201        self
202    }
203
204    /// By default, all metrics are initialized via `metrics::describe_*` macros, setting descriptions and units.
205    ///
206    /// This function disables this initialization.
207    pub fn no_initialize_metrics(mut self) -> Self {
208        self.no_initialize_metrics = true;
209        self
210    }
211}
212
213impl<'a, T, M> MetricLayerBuilder<'a, T, M, LayerOnly> {
214    /// Initialize the builder.
215    pub fn new() -> MetricLayerBuilder<'a, T, M, LayerOnly> {
216        MetricLayerBuilder {
217            _marker: PhantomData,
218            traffic: Traffic::new(),
219            metric_handle: None,
220            no_initialize_metrics: false,
221            metric_prefix: None,
222            enable_body_size: false,
223        }
224    }
225
226    /// Use a prefix for the metrics instead of `axum`. This will use the following
227    /// metric names:
228    ///  - `{prefix}_http_requests_total`
229    ///  - `{prefix}_http_requests_pending`
230    ///  - `{prefix}_http_requests_duration_seconds`
231    ///
232    /// ..and will also use `{prefix}_http_response_body_size`, if response body size tracking is enabled.
233    ///
234    /// This method will take precedence over environment variables.
235    ///
236    /// ## Note
237    ///
238    /// This function inherently changes the metric names, beware to use the appropriate names.
239    /// There're functions in the `utils` module to get them at runtime.
240    ///
241    /// [`utils`]: crate::utils
242    pub fn with_prefix(mut self, prefix: impl Into<Cow<'a, str>>) -> Self {
243        self.metric_prefix = Some(prefix.into().into_owned());
244        self
245    }
246}
247impl<'a, T, M> MetricLayerBuilder<'a, T, M, LayerOnly>
248where
249    M: MakeDefaultHandle<Out = T>,
250{
251    /// Finalize the builder and get the previously registered metric handle out of it.
252    pub fn build(self) -> GenericMetricLayer<'a, T, M> {
253        GenericMetricLayer::from_builder(self)
254    }
255}
256
257impl<'a, T, M> MetricLayerBuilder<'a, T, M, LayerOnly>
258where
259    M: MakeDefaultHandle<Out = T> + Default,
260{
261    /// Attach the default exporter handle to the builder. This is similar to
262    /// initializing with [`GenericMetricLayer::pair`].
263    ///
264    /// After calling this function you can finalize with the [`build_pair`] method, and
265    /// can no longer call [`build`].
266    ///
267    /// [`build`]: crate::MetricLayerBuilder::build
268    /// [`build_pair`]: crate::MetricLayerBuilder::build_pair
269    pub fn with_default_metrics(self) -> MetricLayerBuilder<'a, T, M, Paired> {
270        let mut builder = MetricLayerBuilder::<'_, _, _, Paired>::from_layer_only(self);
271        builder.metric_handle = Some(M::make_default_handle(M::default()));
272        builder
273    }
274}
275impl<'a, T, M> MetricLayerBuilder<'a, T, M, LayerOnly> {
276    /// Attach a custom built exporter handle to the builder that's returned from the passed
277    /// in closure.
278    ///
279    /// ## Example
280    /// ```rust,no_run
281    /// use axum_prometheus::{
282    ///        PrometheusMetricLayerBuilder, AXUM_HTTP_REQUESTS_DURATION_SECONDS, utils::SECONDS_DURATION_BUCKETS,
283    /// };
284    /// use metrics_exporter_prometheus::{Matcher, PrometheusBuilder};
285    ///
286    /// let (metric_layer, metric_handle) = PrometheusMetricLayerBuilder::new()
287    ///     .with_metrics_from_fn(|| {
288    ///         PrometheusBuilder::new()
289    ///             .set_buckets_for_metric(
290    ///                 Matcher::Full(AXUM_HTTP_REQUESTS_DURATION_SECONDS.to_string()),
291    ///                 SECONDS_DURATION_BUCKETS,
292    ///             )
293    ///             .unwrap()
294    ///             .install_recorder()
295    ///             .unwrap()
296    ///     })
297    ///     .build_pair();
298    /// ```
299    /// After calling this function you can finalize with the [`build_pair`] method, and
300    /// can no longer call [`build`].
301    ///
302    /// [`build`]: crate::MetricLayerBuilder::build
303    /// [`build_pair`]: crate::MetricLayerBuilder::build_pair
304    pub fn with_metrics_from_fn(
305        self,
306        f: impl FnOnce() -> T,
307    ) -> MetricLayerBuilder<'a, T, M, Paired> {
308        let mut builder = MetricLayerBuilder::<'_, _, _, Paired>::from_layer_only(self);
309        builder.metric_handle = Some(f());
310        builder
311    }
312}
313
314impl<'a, T, M> MetricLayerBuilder<'a, T, M, Paired> {
315    pub(crate) fn from_layer_only(layer_only: MetricLayerBuilder<'a, T, M, LayerOnly>) -> Self {
316        if let Some(prefix) = layer_only.metric_prefix.as_ref() {
317            set_prefix(prefix);
318        }
319        if !layer_only.no_initialize_metrics {
320            describe_metrics(layer_only.enable_body_size);
321        }
322        MetricLayerBuilder {
323            _marker: PhantomData,
324            traffic: layer_only.traffic,
325            metric_handle: layer_only.metric_handle,
326            no_initialize_metrics: layer_only.no_initialize_metrics,
327            metric_prefix: layer_only.metric_prefix,
328            enable_body_size: layer_only.enable_body_size,
329        }
330    }
331}
332
333impl<'a, T, M> MetricLayerBuilder<'a, T, M, Paired>
334where
335    M: MakeDefaultHandle<Out = T> + Default,
336{
337    /// Finalize the builder and get out the [`GenericMetricLayer`] and the
338    /// exporter handle out of it as a tuple.
339    pub fn build_pair(self) -> (GenericMetricLayer<'a, T, M>, T) {
340        GenericMetricLayer::pair_from_builder(self)
341    }
342}
343
344#[cfg(feature = "prometheus")]
345/// A builder for [`crate::PrometheusMetricLayer`] that enables further customizations.
346pub type PrometheusMetricLayerBuilder<'a, S> =
347    MetricLayerBuilder<'a, PrometheusHandle, crate::Handle, S>;
348
349fn describe_metrics(enable_body_size: bool) {
350    metrics::describe_counter!(
351        crate::utils::requests_total_name(),
352        metrics::Unit::Count,
353        "The number of times a HTTP request was processed."
354    );
355    metrics::describe_gauge!(
356        crate::utils::requests_pending_name(),
357        metrics::Unit::Count,
358        "The number of currently in-flight requests."
359    );
360    metrics::describe_histogram!(
361        crate::utils::requests_duration_name(),
362        metrics::Unit::Seconds,
363        "The distribution of HTTP response times."
364    );
365    if enable_body_size {
366        metrics::describe_histogram!(
367            crate::utils::response_body_size_name(),
368            metrics::Unit::Count,
369            "The distribution of HTTP response body sizes."
370        );
371    }
372}