actix_web_metrics/
lib.rs

1/*!
2[Metrics.rs](https://metrics.rs) integration for [actix-web](https://github.com/actix/actix-web).
3
4This crate tries to adhere to [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/)
5
6The following metrics are supported:
7
8  - [`http.server.request.duration`](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration)
9  - [`http.server.active_requests`](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserveractive_requests)
10  - [`http.server.request.body.size`](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestbodysize)
11  - [`http.server.response.body.size`](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverresponsebodysize)
12
13
14# Usage
15
16First add `actix-web-metrics` to your `Cargo.toml`:
17
18```toml
19[dependencies]
20actix-web-metrics = "x.x.x"
21```
22
23You then instantiate the metrics middleware and pass it to `.wrap()`:
24
25```rust
26use std::collections::HashMap;
27
28use actix_web::{web, App, HttpResponse, HttpServer};
29use actix_web_metrics::{ActixWebMetrics, ActixWebMetricsBuilder};
30use metrics_exporter_prometheus::PrometheusBuilder;
31
32async fn health() -> HttpResponse {
33    HttpResponse::Ok().finish()
34}
35
36#[actix_web::main]
37async fn main() -> std::io::Result<()> {
38    // Register a metrics exporter.
39    // In this case we will just expose a Prometheus metrics endpoint on localhost:9000/metrics
40    //
41    // You can change this to another exporter based on your needs.
42    // See https://github.com/metrics-rs/metrics for more info.
43# if false {
44    PrometheusBuilder::new().install().unwrap();
45# }
46    // Configure & build the Actix-Web middleware layer
47    let metrics = ActixWebMetricsBuilder::new()
48        .build();
49
50# if false {
51    HttpServer::new(move || {
52        App::new()
53            .wrap(metrics.clone())
54            .service(web::resource("/health").to(health))
55    })
56    .bind("127.0.0.1:8080")?
57    .run()
58    .await?;
59# }
60    Ok(())
61}
62```
63
64In the example above we are using the `PrometheusBuilder` from the [metrics-exporter-prometheus](https://docs.rs/metrics-exporter-prometheus/latest/metrics_exporter_prometheus) crate which exposes the metrics via an HTTP endpoint.
65
66A call to the `localhost:9000/metrics` endpoint will expose your metrics:
67
68```shell
69$ curl http://localhost:9000/metrics
70
71# HELP http_server_active_requests Number of active HTTP server requests.
72# TYPE http_server_active_requests gauge
73http_server_active_requests{http_request_method="GET",url_scheme="http"} 1
74
75# HELP http_server_request_duration HTTP request duration in seconds for all requests
76# TYPE http_server_request_duration summary
77http_server_request_duration{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0"} 0.000227207
78http_server_request_duration{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.5"} 0.00022719541927422382
79http_server_request_duration{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.9"} 0.00022719541927422382
80http_server_request_duration{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.95"} 0.00022719541927422382
81http_server_request_duration{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.99"} 0.00022719541927422382
82http_server_request_duration{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.999"} 0.00022719541927422382
83http_server_request_duration{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="1"} 0.000227207
84http_server_request_duration_sum{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1"} 0.000227207
85http_server_request_duration_count{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1"} 1
86
87# HELP http_server_response_body_size HTTP response size in bytes for all requests
88# TYPE http_server_response_body_size summary
89http_server_response_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0"} 0
90http_server_response_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.5"} 0
91http_server_response_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.9"} 0
92http_server_response_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.95"} 0
93http_server_response_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.99"} 0
94http_server_response_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.999"} 0
95http_server_response_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="1"} 0
96http_server_response_body_size_sum{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1"} 0
97http_server_response_body_size_count{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1"} 1
98
99# HELP http_server_request_body_size HTTP request size in bytes for all requests
100# TYPE http_server_request_body_size summary
101http_server_request_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0"} 0
102http_server_request_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.5"} 0
103http_server_request_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.9"} 0
104http_server_request_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.95"} 0
105http_server_request_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.99"} 0
106http_server_request_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="0.999"} 0
107http_server_request_body_size{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1",quantile="1"} 0
108http_server_request_body_size_sum{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1"} 0
109http_server_request_body_size_count{http_route="/health",http_request_method="GET",http_response_status_code="200",network_protocol_name="http",network_protocol_version="1.1"} 1
110```
111
112NOTE: There are 2 important things to note:
113* The `metrics-exporter-prometheus` crate can be swapped for another metrics.rs compatible exporter.
114* The endpoint exposed by `metrics-exporter-prometheus` is not part of the actix web http server.
115
116If you want to expose a prometheus endpoint directly in actix-web see the `prometheus_endpoint.rs` example.
117
118# Features
119
120## Custom metrics
121
122The [metrics.rs](https://docs.rs/metrics/latest/metrics) crate provides macros for custom metrics.
123This crate does interfere with that functionality.
124
125```rust
126use actix_web::{web, App, HttpResponse, HttpServer};
127use actix_web_metrics::{ActixWebMetrics, ActixWebMetricsBuilder};
128use metrics::counter;
129
130async fn health() -> HttpResponse {
131    counter!("my_custom_counter").increment(1);
132    HttpResponse::Ok().finish()
133}
134
135#[actix_web::main]
136async fn main() -> std::io::Result<()> {
137    let metrics = ActixWebMetricsBuilder::new()
138        .build();
139
140# if false {
141        HttpServer::new(move || {
142            App::new()
143                .wrap(metrics.clone())
144                .service(web::resource("/health").to(health))
145        })
146        .bind("127.0.0.1:8080")?
147        .run()
148        .await?;
149# }
150    Ok(())
151}
152```
153
154## Configurable routes pattern cardinality
155
156Let's say you have on your app a route to fetch posts by language and by slug `GET /posts/{language}/{slug}`.
157By default, actix-web-metrics will provide metrics for the whole route with the label `http_route` set to the pattern `/posts/{language}/{slug}`.
158This is great but you cannot differentiate metrics across languages (as there is only a limited set of them).
159Actix-web-metrics can be configured to allow for more cardinality on some route params.
160
161For that you need to add a middleware to pass some [extensions data](https://blog.adamchalmers.com/what-are-extensions/), specifically the [`ActixWebMetricsExtension`] struct that contains the list of params you want to keep cardinality on.
162
163```rust
164use actix_web::{dev::Service, web, HttpMessage, HttpResponse};
165use actix_web_metrics::ActixWebMetricsExtension;
166
167async fn handler() -> HttpResponse {
168    HttpResponse::Ok().finish()
169}
170
171web::resource("/posts/{language}/{slug}")
172    .wrap_fn(|req, srv| {
173        req.extensions_mut().insert::<ActixWebMetricsExtension>(
174            ActixWebMetricsExtension { cardinality_keep_params: vec!["language".to_string()] }
175        );
176        srv.call(req)
177    })
178    .route(web::get().to(handler));
179```
180
181See the full example `with_cardinality_on_params.rs`.
182
183## Configurable metric names
184
185If you want to rename the default metrics, you can use [`ActixWebMetricsConfig`] to do so.
186
187```rust
188use actix_web_metrics::{ActixWebMetricsBuilder, ActixWebMetricsConfig};
189
190ActixWebMetricsBuilder::new()
191    .metrics_config(
192        ActixWebMetricsConfig::default()
193           .http_server_request_duration_name("my_http_request_duration")
194           .http_server_request_body_size_name("my_http_server_request_body_size_name")
195           .http_server_response_body_size_name("my_http_server_response_body_size_name")
196           .http_server_active_requests_name("my_http_server_active_requests_name"),
197    )
198    .build();
199```
200
201See full example `configuring_default_metrics.rs`.
202
203## Masking unmatched requests
204
205By default, if a request path is not matched to an Actix Web route, it will be masked as `UNKNOWN`.
206This is useful to avoid producing lots of useless metrics due to bots or malious actors.
207
208This can be configured in the following ways:
209* `mask_unmatched_patterns()` can be used to change the `http_route` label to something other than `UNKNOWN`.
210* `disable_unmatched_pattern_masking()` can be used to disable this masking functionality.
211
212```rust,no_run
213use actix_web_metrics::ActixWebMetricsBuilder;
214
215ActixWebMetricsBuilder::new()
216    .mask_unmatched_patterns("UNMATCHED")
217    // or .disable_unmatched_pattern_masking()
218    .build();
219```
220
221The above will convert all `/<nonexistent-path>` into `UNMATCHED`:
222
223```text
224http_requests_duration_seconds_sum{http_route="/favicon.ico",http_request_method="GET",http_response_status="400"} 0.000424898
225```
226
227becomes
228
229```text
230http_requests_duration_seconds_sum{http_route="UNMATCHED",http_request_method="GET",http_response_status="400"} 0.000424898
231```
232*/
233#![deny(missing_docs)]
234
235use actix_web::http::Uri;
236use log::warn;
237use metrics::{describe_gauge, describe_histogram, gauge, histogram, Unit};
238use std::collections::{HashMap, HashSet};
239use std::future::{ready, Future, Ready};
240use std::marker::PhantomData;
241use std::pin::Pin;
242use std::sync::Arc;
243use std::task::{Context, Poll};
244use std::time::Instant;
245
246use actix_web::{
247    body::{BodySize, MessageBody},
248    dev::{self, Service, ServiceRequest, ServiceResponse, Transform},
249    http::{Method, StatusCode, Version},
250    web::Bytes,
251    Error, HttpMessage,
252};
253use futures_core::ready;
254use pin_project_lite::pin_project;
255
256use regex::RegexSet;
257use strfmt::strfmt;
258
259/// ActixWebMetricsExtension define middleware and config struct to change the behaviour of the metrics
260/// struct to define some particularities
261#[derive(Debug, Clone)]
262pub struct ActixWebMetricsExtension {
263    /// list of params where the cardinality matters
264    pub cardinality_keep_params: Vec<String>,
265}
266
267/// Builder to create new [`ActixWebMetrics`] struct.
268#[derive(Debug)]
269pub struct ActixWebMetricsBuilder {
270    namespace: Option<String>,
271    const_labels: HashMap<String, String>,
272    exclude: HashSet<String>,
273    exclude_regex: RegexSet,
274    exclude_status: HashSet<StatusCode>,
275    unmatched_patterns_mask: Option<String>,
276    metrics_config: ActixWebMetricsConfig,
277}
278
279impl ActixWebMetricsBuilder {
280    /// Create new `ActixWebMetricsBuilder`
281    pub fn new() -> Self {
282        Self {
283            namespace: None,
284            const_labels: HashMap::new(),
285            exclude: HashSet::new(),
286            exclude_regex: RegexSet::empty(),
287            exclude_status: HashSet::new(),
288            unmatched_patterns_mask: Some("UNKNOWN".to_string()),
289            metrics_config: ActixWebMetricsConfig::default(),
290        }
291    }
292
293    /// Set labels to add on every metrics
294    pub fn const_labels(mut self, value: HashMap<String, String>) -> Self {
295        self.const_labels = value;
296        self
297    }
298
299    /// Set namespace
300    pub fn namespace<T: Into<String>>(mut self, value: T) -> Self {
301        self.namespace = Some(value.into());
302        self
303    }
304
305    /// Ignore and do not record metrics for specified path.
306    pub fn exclude<T: Into<String>>(mut self, path: T) -> Self {
307        self.exclude.insert(path.into());
308        self
309    }
310
311    /// Ignore and do not record metrics for paths matching the regex.
312    pub fn exclude_regex<T: Into<String>>(mut self, path: T) -> Self {
313        let mut patterns = self.exclude_regex.patterns().to_vec();
314        patterns.push(path.into());
315        self.exclude_regex = RegexSet::new(patterns).unwrap();
316        self
317    }
318
319    /// Ignore and do not record metrics for paths returning the status code.
320    pub fn exclude_status<T: Into<StatusCode>>(mut self, status: T) -> Self {
321        self.exclude_status.insert(status.into());
322        self
323    }
324
325    /// Replaces the request path with the supplied mask if no actix-web handler is matched
326    ///
327    /// Defaults to `UNKNOWN`
328    pub fn mask_unmatched_patterns<T: Into<String>>(mut self, mask: T) -> Self {
329        self.unmatched_patterns_mask = Some(mask.into());
330        self
331    }
332
333    /// Disable masking of unmatched patterns.
334    ///
335    /// WARNING:This may lead to unbounded cardinality for unmatched requests. (potential DoS)
336    pub fn disable_unmatched_pattern_masking(mut self) -> Self {
337        self.unmatched_patterns_mask = None;
338        self
339    }
340
341    /// Set metrics configuration
342    pub fn metrics_config(mut self, value: ActixWebMetricsConfig) -> Self {
343        self.metrics_config = value;
344        self
345    }
346
347    /// Instantiate `ActixWebMetrics` struct
348    ///
349    /// WARNING: This call purposefully leaks the memory of metrics and label names to avoid
350    /// allocations during runtime. Avoid calling more than once.
351    pub fn build(self) -> ActixWebMetrics {
352        let namespace_prefix = if let Some(ns) = self.namespace {
353            format!("{ns}_")
354        } else {
355            "".to_string()
356        };
357
358        let http_server_request_duration_name = format!(
359            "{namespace_prefix}{}",
360            self.metrics_config.http_server_request_duration_name
361        );
362        describe_histogram!(
363            http_server_request_duration_name.clone(),
364            Unit::Seconds,
365            "HTTP request duration in seconds for all requests"
366        );
367
368        let http_server_request_body_size_name = format!(
369            "{namespace_prefix}{}",
370            self.metrics_config.http_server_request_body_size_name
371        );
372        describe_histogram!(
373            http_server_request_body_size_name.clone(),
374            Unit::Bytes,
375            "HTTP request size in bytes for all requests"
376        );
377
378        let http_server_response_body_size_name = format!(
379            "{namespace_prefix}{}",
380            self.metrics_config.http_server_response_body_size_name
381        );
382        describe_histogram!(
383            http_server_response_body_size_name.clone(),
384            Unit::Bytes,
385            "HTTP response size in bytes for all requests"
386        );
387
388        let http_server_active_requests_name = format!(
389            "{namespace_prefix}{}",
390            self.metrics_config.http_server_active_requests_name
391        );
392        describe_gauge!(
393            http_server_active_requests_name.clone(),
394            "Number of active HTTP server requests."
395        );
396
397        let mut const_labels: Vec<(&'static str, String)> = self
398            .const_labels
399            .iter()
400            .map(|(k, v)| {
401                let k: &'static str = Box::leak(Box::new(k.clone()));
402                (k, v.clone())
403            })
404            .collect();
405        const_labels.sort_by_key(|v| v.0);
406
407        ActixWebMetrics {
408            inner: Arc::new(ActixWebMetricsInner {
409                exclude: self.exclude,
410                exclude_regex: self.exclude_regex,
411                exclude_status: self.exclude_status,
412                unmatched_patterns_mask: self.unmatched_patterns_mask,
413                names: MetricsMetadata {
414                    http_server_request_duration: Box::leak(Box::new(
415                        http_server_request_duration_name,
416                    )),
417                    http_server_request_body_size: Box::leak(Box::new(
418                        http_server_request_body_size_name,
419                    )),
420                    http_server_response_body_size: Box::leak(Box::new(
421                        http_server_response_body_size_name,
422                    )),
423                    http_server_active_requests: Box::leak(Box::new(
424                        http_server_active_requests_name,
425                    )),
426                    http_route: Box::leak(Box::new(self.metrics_config.labels.http_route)),
427                    http_request_method: Box::leak(Box::new(
428                        self.metrics_config.labels.http_request_method,
429                    )),
430                    http_response_status_code: Box::leak(Box::new(
431                        self.metrics_config.labels.http_response_status_code,
432                    )),
433                    network_protocol_name: Box::leak(Box::new(
434                        self.metrics_config.labels.network_protocol_name,
435                    )),
436                    network_protocol_version: Box::leak(Box::new(
437                        self.metrics_config.labels.network_protocol_version,
438                    )),
439                    url_scheme: Box::leak(Box::new(self.metrics_config.labels.url_scheme)),
440                    const_labels,
441                },
442            }),
443        }
444    }
445}
446
447impl Default for ActixWebMetricsBuilder {
448    fn default() -> Self {
449        Self::new()
450    }
451}
452
453/// Configuration for the labels used in metrics
454#[derive(Debug, Clone)]
455pub struct LabelsConfig {
456    http_route: String,
457    http_request_method: String,
458    http_response_status_code: String,
459    network_protocol_name: String,
460    network_protocol_version: String,
461    url_scheme: String,
462}
463
464impl Default for LabelsConfig {
465    fn default() -> Self {
466        Self {
467            http_route: String::from("http.route"),
468            http_request_method: String::from("http.request.method"),
469            http_response_status_code: String::from("http.response.status_code"),
470            network_protocol_name: String::from("network.protocol.name"),
471            network_protocol_version: String::from("network.protocol.version"),
472            url_scheme: String::from("url.scheme"),
473        }
474    }
475}
476
477impl LabelsConfig {
478    /// set http method label
479    pub fn http_request_method<T: Into<String>>(mut self, name: T) -> Self {
480        self.http_request_method = name.into();
481        self
482    }
483
484    /// set http route label
485    pub fn http_route<T: Into<String>>(mut self, name: T) -> Self {
486        self.http_route = name.into();
487        self
488    }
489
490    /// set http status label
491    pub fn http_response_status_code<T: Into<String>>(mut self, name: T) -> Self {
492        self.http_response_status_code = name.into();
493        self
494    }
495
496    /// set network protocol name label
497    pub fn network_protocol_name<T: Into<String>>(mut self, name: T) -> Self {
498        self.network_protocol_name = name.into();
499        self
500    }
501
502    /// set network protocol version label
503    pub fn network_protocol_version<T: Into<String>>(mut self, name: T) -> Self {
504        self.network_protocol_version = name.into();
505        self
506    }
507
508    /// set url scheme label
509    pub fn url_scheme<T: Into<String>>(mut self, name: T) -> Self {
510        self.url_scheme = name.into();
511        self
512    }
513}
514
515/// Configuration for the collected metrics
516///
517/// Stores individual metric configuration objects
518#[derive(Debug, Clone)]
519pub struct ActixWebMetricsConfig {
520    http_server_request_duration_name: String,
521    http_server_request_body_size_name: String,
522    http_server_response_body_size_name: String,
523    http_server_active_requests_name: String,
524    labels: LabelsConfig,
525}
526
527impl Default for ActixWebMetricsConfig {
528    fn default() -> Self {
529        Self {
530            http_server_request_duration_name: String::from("http.server.request.duration"),
531            http_server_request_body_size_name: String::from("http.server.request.body.size"),
532            http_server_response_body_size_name: String::from("http.server.response.body.size"),
533            http_server_active_requests_name: String::from("http.server.active_requests"),
534            labels: LabelsConfig::default(),
535        }
536    }
537}
538
539impl ActixWebMetricsConfig {
540    /// Set the labels collected for the metrics
541    pub fn labels(mut self, labels: LabelsConfig) -> Self {
542        self.labels = labels;
543        self
544    }
545
546    /// Set name for `http.server.request.duration` metric
547    pub fn http_server_request_duration_name<T: Into<String>>(mut self, name: T) -> Self {
548        self.http_server_request_duration_name = name.into();
549        self
550    }
551
552    /// Set name for `http.server.request.body.size` metric
553    pub fn http_server_request_body_size_name<T: Into<String>>(mut self, name: T) -> Self {
554        self.http_server_request_body_size_name = name.into();
555        self
556    }
557
558    /// Set name for `http.server.response.body.size` metric
559    pub fn http_server_response_body_size_name<T: Into<String>>(mut self, name: T) -> Self {
560        self.http_server_response_body_size_name = name.into();
561        self
562    }
563
564    /// Set name for `http.server.active_requests` metric
565    pub fn http_server_active_requests_name<T: Into<String>>(mut self, name: T) -> Self {
566        self.http_server_active_requests_name = name.into();
567        self
568    }
569}
570
571/// Static references to variable metrics/label names.
572/// This config primarily exists to avoid allocations during execution.
573#[derive(Debug, Clone)]
574struct MetricsMetadata {
575    // metric names
576    http_server_request_duration: &'static str,
577    http_server_request_body_size: &'static str,
578    http_server_response_body_size: &'static str,
579    http_server_active_requests: &'static str,
580    // label names
581    http_route: &'static str,
582    http_request_method: &'static str,
583    http_response_status_code: &'static str,
584    network_protocol_name: &'static str,
585    network_protocol_version: &'static str,
586    url_scheme: &'static str,
587    const_labels: Vec<(&'static str, String)>,
588}
589
590/// An actix-web middleware the records metrics.
591///
592/// See the module documentation for more details
593#[derive(Clone)]
594#[must_use = "must be set up as middleware for actix-web"]
595pub struct ActixWebMetrics {
596    inner: Arc<ActixWebMetricsInner>,
597}
598
599struct ActixWebMetricsInner {
600    pub(crate) names: MetricsMetadata,
601
602    pub(crate) exclude: HashSet<String>,
603    pub(crate) exclude_regex: RegexSet,
604    pub(crate) exclude_status: HashSet<StatusCode>,
605    pub(crate) unmatched_patterns_mask: Option<String>,
606}
607
608impl ActixWebMetrics {
609    fn pre_request_update_metrics(&self, req: &ServiceRequest) {
610        let this = &*self.inner;
611
612        let mut labels = Vec::with_capacity(2 + this.names.const_labels.len());
613        labels.push((
614            this.names.http_request_method,
615            req.method().as_str().to_string(),
616        ));
617        labels.push((this.names.url_scheme, url_scheme(&req.uri()).to_string()));
618        for (k, v) in &this.names.const_labels {
619            labels.push((k, v.clone()));
620        }
621
622        gauge!(this.names.http_server_active_requests, &labels).increment(1);
623    }
624
625    #[allow(clippy::too_many_arguments)]
626    fn post_request_update_metrics(
627        &self,
628        http_version: Version,
629        mixed_pattern: &str,
630        fallback_pattern: &str,
631        method: &Method,
632        status: StatusCode,
633        scheme: &str,
634        clock: Instant,
635        was_path_matched: bool,
636        request_size: usize,
637        response_size: usize,
638    ) {
639        let this = &*self.inner;
640
641        // NOTE: active_requests cannot be skips as we need to decrement the increment we did that
642        // the beginning of the request.
643        {
644            let mut active_request_labels = Vec::with_capacity(2 + this.names.const_labels.len());
645            active_request_labels
646                .push((this.names.http_request_method, method.as_str().to_string()));
647            active_request_labels.push((this.names.url_scheme, scheme.to_string()));
648            for (k, v) in &this.names.const_labels {
649                active_request_labels.push((k, v.clone()));
650            }
651            gauge!(
652                this.names.http_server_active_requests,
653                &active_request_labels
654            )
655            .decrement(1);
656        }
657
658        if this.exclude.contains(mixed_pattern)
659            || this.exclude_regex.is_match(mixed_pattern)
660            || this.exclude_status.contains(&status)
661        {
662            return;
663        }
664
665        // do not record mixed patterns that were considered invalid by the server
666        let final_pattern = if fallback_pattern != mixed_pattern && (status == 404 || status == 405)
667        {
668            fallback_pattern
669        } else {
670            mixed_pattern
671        };
672
673        let final_pattern = if was_path_matched {
674            final_pattern
675        } else if let Some(mask) = &this.unmatched_patterns_mask {
676            mask
677        } else {
678            final_pattern
679        };
680
681        let mut labels = Vec::with_capacity(5 + this.names.const_labels.len());
682        labels.push((this.names.http_route, final_pattern.to_string()));
683        labels.push((this.names.http_request_method, method.as_str().to_string()));
684        labels.push((
685            this.names.http_response_status_code,
686            status.as_str().to_string(),
687        ));
688        labels.push((this.names.network_protocol_name, "http".to_string()));
689
690        if let Some(http_version) = Self::http_version_label(http_version) {
691            labels.push((
692                this.names.network_protocol_version,
693                http_version.to_string(),
694            ));
695        }
696
697        for (k, v) in &this.names.const_labels {
698            labels.push((k, v.clone()));
699        }
700
701        let elapsed = clock.elapsed();
702        let duration =
703            (elapsed.as_secs() as f64) + f64::from(elapsed.subsec_nanos()) / 1_000_000_000_f64;
704        histogram!(this.names.http_server_request_duration, &labels).record(duration);
705        histogram!(this.names.http_server_request_body_size, &labels).record(request_size as f64);
706        histogram!(this.names.http_server_response_body_size, &labels).record(response_size as f64);
707    }
708
709    fn http_version_label(version: Version) -> Option<&'static str> {
710        let v = match version {
711            v if v == Version::HTTP_09 => "0.9",
712            v if v == Version::HTTP_10 => "1.0",
713            v if v == Version::HTTP_11 => "1.1",
714            v if v == Version::HTTP_2 => "2",
715            v if v == Version::HTTP_3 => "3",
716            _ => return None,
717        };
718
719        Some(v)
720    }
721}
722
723impl<S, B> Transform<S, ServiceRequest> for ActixWebMetrics
724where
725    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
726{
727    type Response = ServiceResponse<StreamLog<B>>;
728    type Error = Error;
729    type InitError = ();
730    type Transform = MetricsMiddleware<S>;
731    type Future = Ready<Result<Self::Transform, Self::InitError>>;
732
733    fn new_transform(&self, service: S) -> Self::Future {
734        ready(Ok(MetricsMiddleware {
735            service,
736            inner: self.clone(),
737        }))
738    }
739}
740
741pin_project! {
742    #[doc(hidden)]
743    pub struct LoggerResponse<S>
744        where
745        S: Service<ServiceRequest>,
746    {
747        #[pin]
748        fut: S::Future,
749        time: Instant,
750        inner: ActixWebMetrics,
751        _t: PhantomData<()>,
752    }
753}
754
755impl<S, B> Future for LoggerResponse<S>
756where
757    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
758{
759    type Output = Result<ServiceResponse<StreamLog<B>>, Error>;
760
761    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
762        let this = self.project();
763
764        let res = match ready!(this.fut.poll(cx)) {
765            Ok(res) => res,
766            Err(e) => return Poll::Ready(Err(e)),
767        };
768
769        let time = *this.time;
770        let req = res.request();
771        let method = req.method().clone();
772        let version = req.version();
773        let was_path_matched = req.match_pattern().is_some();
774
775        // get metrics config for this specific route
776        // piece of code to allow for more cardinality
777        let params_keep_path_cardinality =
778            match req.extensions_mut().get::<ActixWebMetricsExtension>() {
779                Some(config) => config.cardinality_keep_params.clone(),
780                None => vec![],
781            };
782
783        let full_pattern = req.match_pattern();
784        let path = req.path().to_string();
785        let fallback_pattern = full_pattern.clone().unwrap_or(path.clone());
786
787        // mixed_pattern is the final path used as label value in metrics
788        let mixed_pattern = match full_pattern {
789            None => path.clone(),
790            Some(full_pattern) => {
791                let mut params: HashMap<String, String> = HashMap::new();
792
793                for (key, val) in req.match_info().iter() {
794                    if params_keep_path_cardinality.contains(&key.to_string()) {
795                        params.insert(key.to_string(), val.to_string());
796                        continue;
797                    }
798                    params.insert(key.to_string(), format!("{{{key}}}"));
799                }
800
801                if let Ok(mixed_cardinality_pattern) = strfmt(&full_pattern, &params) {
802                    mixed_cardinality_pattern
803                } else {
804                    warn!("Cannot build mixed cardinality pattern {full_pattern}, with params {params:?}");
805                    full_pattern
806                }
807            }
808        };
809
810        // Get request size from Content-Length header
811        let request_size = req
812            .headers()
813            .get("content-length")
814            .and_then(|v| v.to_str().ok())
815            .and_then(|v| v.parse::<usize>().ok())
816            .unwrap_or(0);
817
818        let scheme = url_scheme(&req.uri()).to_string();
819        let inner = this.inner.clone();
820        Poll::Ready(Ok(res.map_body(move |head, body| StreamLog {
821            body,
822            response_size: 0,
823            request_size,
824            clock: time,
825            inner,
826            status: head.status,
827            scheme,
828            mixed_pattern,
829            fallback_pattern,
830            method,
831            version,
832            was_path_matched,
833        })))
834    }
835}
836
837/// Middleware service for [`ActixWebMetrics`]
838#[doc(hidden)]
839pub struct MetricsMiddleware<S> {
840    service: S,
841    inner: ActixWebMetrics,
842}
843
844impl<S, B> Service<ServiceRequest> for MetricsMiddleware<S>
845where
846    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
847{
848    type Response = ServiceResponse<StreamLog<B>>;
849    type Error = S::Error;
850    type Future = LoggerResponse<S>;
851
852    dev::forward_ready!(service);
853
854    fn call(&self, req: ServiceRequest) -> Self::Future {
855        self.inner.pre_request_update_metrics(&req);
856
857        LoggerResponse {
858            fut: self.service.call(req),
859            time: Instant::now(),
860            inner: self.inner.clone(),
861            _t: PhantomData,
862        }
863    }
864}
865
866pin_project! {
867    #[doc(hidden)]
868    pub struct StreamLog<B> {
869        #[pin]
870        body: B,
871        response_size: usize,
872        request_size: usize,
873        clock: Instant,
874        inner: ActixWebMetrics,
875        status: StatusCode,
876        scheme: String,
877        // a route pattern with some params not-filled and some params filled in by user-defined
878        mixed_pattern: String,
879        fallback_pattern: String,
880        method: Method,
881        version: Version,
882        was_path_matched: bool
883    }
884
885
886    impl<B> PinnedDrop for StreamLog<B> {
887        fn drop(this: Pin<&mut Self>) {
888            // update the metrics for this request at the very end of responding
889            this.inner
890                .post_request_update_metrics(this.version, &this.mixed_pattern, &this.fallback_pattern, &this.method, this.status, &this.scheme, this.clock, this.was_path_matched, this.request_size, this.response_size);
891        }
892    }
893}
894
895impl<B: MessageBody> MessageBody for StreamLog<B> {
896    type Error = B::Error;
897
898    fn size(&self) -> BodySize {
899        self.body.size()
900    }
901
902    fn poll_next(
903        self: Pin<&mut Self>,
904        cx: &mut Context<'_>,
905    ) -> Poll<Option<Result<Bytes, Self::Error>>> {
906        let this = self.project();
907        match ready!(this.body.poll_next(cx)) {
908            Some(Ok(chunk)) => {
909                *this.response_size += chunk.len();
910                Poll::Ready(Some(Ok(chunk)))
911            }
912            Some(Err(err)) => Poll::Ready(Some(Err(err))),
913            None => Poll::Ready(None),
914        }
915    }
916}
917
918fn url_scheme(uri: &Uri) -> &str {
919    uri.scheme().map(|s| s.as_str()).unwrap_or("http")
920}