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}