actix_web_metrics/
lib.rs

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