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, ¶ms) {
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}