actix_web_prometheus/
lib.rs

1/*!
2Prometheus instrumentation for [actix-web](https://github.com/actix/actix-web).
3This middleware is inspired by and forked from [actix-web-prom](https://github.com/nlopes/actix-web-prom).
4By default three metrics are tracked (this assumes the namespace `actix_web_prometheus`):
5  - `actix_web_prometheus_incoming_requests` (labels: endpoint, method, status): the total number
6   of HTTP requests handled by the actix HttpServer.
7  - `actix_web_prometheus_response_code` (labels: endpoint, method, statuscode, type): Response codes
8   of all HTTP requests handled by the actix HttpServer.
9  - `actix_web_prometheus_response_time` (labels: endpoint, method, status): Total the request duration
10   of all HTTP requests handled by the actix HttpServer.
11# Usage
12First add `actix-web-prom` to your `Cargo.toml`:
13```toml
14[dependencies]
15actix-web-prometheus = "0.1.0-beta.8"
16```
17You then instantiate the prometheus middleware and pass it to `.wrap()`:
18```rust
19use std::collections::HashMap;
20use actix_web::{web, App, HttpResponse, HttpServer};
21use actix_web_prometheus::{PrometheusMetrics, PrometheusMetricsBuilder};
22fn health() -> HttpResponse {
23    HttpResponse::Ok().finish()
24}
25#[actix_web::main]
26async fn main() -> std::io::Result<()> {
27    let mut labels = HashMap::new();
28    labels.insert("label1".to_string(), "value1".to_string());
29    let prometheus = PrometheusMetricsBuilder::new("api")
30        .endpoint("/metrics")
31        .const_labels(labels)
32        .build()
33        .unwrap();
34# if false {
35        HttpServer::new(move || {
36            App::new()
37                .wrap(prometheus.clone())
38                .service(web::resource("/health").to(health))
39        })
40        .bind("127.0.0.1:8080")?
41        .run()
42        .await?;
43# }
44    Ok(())
45}
46```
47Using the above as an example, a few things are worth mentioning:
48 - `api` is the metrics namespace
49 - `/metrics` will be auto exposed (GET requests only) with Content-Type header `content-type: text/plain; version=0.0.4; charset=utf-8`
50 - `Some(labels)` is used to add fixed labels to the metrics; `None` can be passed instead
51  if no additional labels are necessary.
52A call to the /metrics endpoint will expose your metrics:
53```shell
54$ curl http://localhost:8080/metrics
55# HELP actix_web_prometheus_incoming_requests Incoming Requests
56# TYPE actix_web_prometheus_incoming_requests counter
57actix_web_prometheus_incoming_requests{endpoint="/metrics",method="GET",status="200"} 23
58# HELP actix_web_prometheus_response_code Response Codes
59# TYPE actix_web_prometheus_response_code counter
60actix_web_prometheus_response_code{endpoint="/metrics",method="GET",statuscode="200",type="200"} 23
61# HELP actix_web_prometheus_response_time Response Times
62# TYPE actix_web_prometheus_response_time histogram
63actix_web_prometheus_response_time_bucket{endpoint="/metrics",method="GET",status="200",le="0.005"} 23
64actix_web_prometheus_response_time_bucket{endpoint="/metrics",method="GET",status="200",le="0.01"} 23
65actix_web_prometheus_response_time_bucket{endpoint="/metrics",method="GET",status="200",le="0.025"} 23
66actix_web_prometheus_response_time_bucket{endpoint="/metrics",method="GET",status="200",le="0.05"} 23
67actix_web_prometheus_response_time_bucket{endpoint="/metrics",method="GET",status="200",le="0.1"} 23
68actix_web_prometheus_response_time_bucket{endpoint="/metrics",method="GET",status="200",le="0.25"} 23
69actix_web_prometheus_response_time_bucket{endpoint="/metrics",method="GET",status="200",le="0.5"} 23
70actix_web_prometheus_response_time_bucket{endpoint="/metrics",method="GET",status="200",le="1"} 23
71actix_web_prometheus_response_time_bucket{endpoint="/metrics",method="GET",status="200",le="2.5"} 23
72actix_web_prometheus_response_time_bucket{endpoint="/metrics",method="GET",status="200",le="5"} 23
73actix_web_prometheus_response_time_bucket{endpoint="/metrics",method="GET",status="200",le="10"} 23
74actix_web_prometheus_response_time_bucket{endpoint="/metrics",method="GET",status="200",le="+Inf"} 23
75actix_web_prometheus_response_time_sum{endpoint="/metrics",method="GET",status="200"} 0.00410981
76actix_web_prometheus_response_time_count{endpoint="/metrics",method="GET",status="200"} 23
77```
78
79## Features
80If you enable `process` feature of this crate, default process metrics will also be collected.
81[Default process metrics](https://prometheus.io/docs/instrumenting/writing_clientlibs/#process-metrics)
82
83```shell
84# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
85# TYPE process_cpu_seconds_total counter
86process_cpu_seconds_total 0.22
87# HELP process_max_fds Maximum number of open file descriptors.
88# TYPE process_max_fds gauge
89process_max_fds 1048576
90# HELP process_open_fds Number of open file descriptors.
91# TYPE process_open_fds gauge
92process_open_fds 78
93# HELP process_resident_memory_bytes Resident memory size in bytes.
94# TYPE process_resident_memory_bytes gauge
95process_resident_memory_bytes 17526784
96# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
97# TYPE process_start_time_seconds gauge
98process_start_time_seconds 1628105774.92
99# HELP process_virtual_memory_bytes Virtual memory size in bytes.
100# TYPE process_virtual_memory_bytes gauge
101process_virtual_memory_bytes 1893163008
102```
103
104## Custom metrics
105You instantiate `PrometheusMetrics` and then use its `.registry` to register your custom
106metric (in this case, we use a `IntCounterVec`).
107Then you can pass this counter through `.data()` to have it available within the resource
108responder.
109```rust
110use actix_web::{web, App, HttpResponse, HttpServer};
111use actix_web_prometheus::{PrometheusMetrics, PrometheusMetricsBuilder};
112use prometheus::{opts, IntCounterVec};
113fn health(counter: web::Data<IntCounterVec>) -> HttpResponse {
114    counter.with_label_values(&["endpoint", "method", "status"]).inc();
115    HttpResponse::Ok().finish()
116}
117#[actix_web::main]
118async fn main() -> std::io::Result<()> {
119    let prometheus = PrometheusMetricsBuilder::new("api")
120        .endpoint("/metrics")
121        .build()
122        .unwrap();
123    let counter_opts = opts!("counter", "some random counter").namespace("api");
124    let counter = IntCounterVec::new(counter_opts, &["endpoint", "method", "status"]).unwrap();
125    prometheus
126        .registry
127        .register(Box::new(counter.clone()))
128        .unwrap();
129# if false {
130        HttpServer::new(move || {
131            App::new()
132                .wrap(prometheus.clone())
133                .app_data(web::Data::new(counter.clone()))
134                .service(web::resource("/health").to(health))
135        })
136        .bind("127.0.0.1:8080")?
137        .run()
138        .await?;
139# }
140    Ok(())
141}
142```
143 */
144
145pub mod error;
146pub use error::Error;
147
148use actix_web::{
149    dev::{Service, ServiceRequest, ServiceResponse, Transform},
150    http::{header::CONTENT_TYPE, Method, StatusCode},
151    web::Bytes,
152    Error as ActixError,
153};
154use futures_lite::future::{ready, Ready};
155use futures_lite::ready;
156use prometheus::{HistogramOpts, HistogramVec, IntCounterVec, Opts, Registry};
157use std::error::Error as StdError;
158use std::future::Future;
159use std::pin::Pin;
160use std::rc::Rc;
161use std::task::{Context, Poll};
162
163#[derive(Debug)]
164/// Builder to create new PrometheusMetrics struct.HistogramVec
165///
166/// It allow set optional parameters like registry, buckets, etc.
167pub struct PrometheusMetricsBuilder {
168    namespace: String,
169    endpoint: Option<String>,
170    const_labels: HashMap<String, String>,
171    registry: Option<Registry>,
172    buckets: Vec<f64>,
173}
174
175impl PrometheusMetricsBuilder {
176    /// Create new PrometheusMetricsBuilder
177    ///
178    /// namespace example: "actix"
179    pub fn new(namespace: &str) -> Self {
180        Self {
181            namespace: namespace.into(),
182            endpoint: None,
183            const_labels: HashMap::new(),
184            registry: Some(Registry::new()),
185            buckets: prometheus::DEFAULT_BUCKETS.to_vec(),
186        }
187    }
188
189    /// Set actix web endpoint
190    ///
191    /// Example: "/metrics"
192    pub fn endpoint(mut self, value: &str) -> Self {
193        self.endpoint = Some(value.into());
194        self
195    }
196
197    /// Set histogram buckets
198    pub fn buckets(mut self, value: &[f64]) -> Self {
199        self.buckets = value.to_vec();
200        self
201    }
202
203    /// Set labels to add on every metrics
204    pub fn const_labels(mut self, value: HashMap<String, String>) -> Self {
205        self.const_labels = value;
206        self
207    }
208
209    /// Set registry
210    ///
211    /// By default one is set and is internal to PrometheusMetrics
212    pub fn registry(mut self, value: Registry) -> Self {
213        self.registry = Some(value);
214        self
215    }
216
217    /// Instantiate PrometheusMetrics struct
218    pub fn build(self) -> Result<PrometheusMetrics, Error> {
219        let registry = match self.registry {
220            Some(registry) => registry,
221            None => Registry::new(),
222        };
223
224        let incoming_requests = IntCounterVec::new(
225            Opts::new("incoming_requests", "Incoming Requests")
226                .namespace(&self.namespace)
227                .const_labels(self.const_labels.clone()),
228            &["endpoint", "method", "status"],
229        )?;
230
231        let response_time = HistogramVec::new(
232            HistogramOpts::new("response_time", "Response Times")
233                .namespace(&self.namespace)
234                .const_labels(self.const_labels.clone())
235                .buckets(self.buckets.clone()),
236            &["endpoint", "method", "status"],
237        )?;
238
239        let response_codes = IntCounterVec::new(
240            Opts::new("response_code", "Response Codes")
241                .namespace(&self.namespace)
242                .const_labels(self.const_labels.clone()),
243            &["endpoint", "method", "statuscode", "type"],
244        )?;
245
246        registry.register(Box::new(incoming_requests.clone()))?;
247        registry.register(Box::new(response_time.clone()))?;
248        registry.register(Box::new(response_codes.clone()))?;
249
250        Ok(PrometheusMetrics {
251            clock: quanta::Clock::new(),
252            registry,
253            namespace: self.namespace,
254            endpoint: self.endpoint,
255            const_labels: self.const_labels,
256            incoming_requests,
257            response_time,
258            response_codes,
259        })
260    }
261}
262
263#[derive(Clone, Debug)]
264pub struct PrometheusMetrics {
265    pub registry: Registry,
266    pub(crate) namespace: String,
267    pub(crate) endpoint: Option<String>,
268    pub(crate) const_labels: HashMap<String, String>,
269    pub(crate) clock: quanta::Clock,
270    pub(crate) incoming_requests: IntCounterVec,
271    pub(crate) response_time: HistogramVec,
272    pub(crate) response_codes: IntCounterVec,
273}
274
275impl PrometheusMetrics {
276    fn metrics(&self) -> String {
277        use prometheus::{Encoder, TextEncoder};
278
279        let mut buffer = vec![];
280        TextEncoder::new()
281            .encode(&self.registry.gather(), &mut buffer)
282            .unwrap();
283
284        #[cfg(feature = "process")]
285        {
286            let mut process_metrics = vec![];
287            TextEncoder::new()
288                .encode(&prometheus::gather(), &mut process_metrics)
289                .unwrap();
290
291            buffer.extend_from_slice(&process_metrics);
292        }
293
294        String::from_utf8(buffer).unwrap()
295    }
296
297    fn matches(&self, path: &str, method: &Method) -> bool {
298        if self.endpoint.is_some() {
299            self.endpoint.as_ref().unwrap() == path && method == Method::GET
300        } else {
301            false
302        }
303    }
304
305    fn update_metrics(
306        &self,
307        path: &str,
308        method: &Method,
309        status_code: StatusCode,
310        start: u64,
311        end: u64,
312    ) {
313        let method = method.to_string();
314        let status = status_code.as_u16().to_string();
315
316        let elapsed = self.clock.delta(start, end);
317        let duration = elapsed.as_secs_f64();
318
319        self.response_time
320            .with_label_values(&[path, &method, &status])
321            .observe(duration);
322
323        self.incoming_requests
324            .with_label_values(&[path, &method, &status])
325            .inc();
326
327        match status_code.as_u16() {
328            500..=599 => self
329                .response_codes
330                .with_label_values(&[path, &method, &status, "500"])
331                .inc(),
332            400..=499 => self
333                .response_codes
334                .with_label_values(&[path, &method, &status, "400"])
335                .inc(),
336            300..=399 => self
337                .response_codes
338                .with_label_values(&[path, &method, &status, "300"])
339                .inc(),
340            200..=299 => self
341                .response_codes
342                .with_label_values(&[path, &method, &status, "200"])
343                .inc(),
344            100..=199 => self
345                .response_codes
346                .with_label_values(&[path, &method, &status, "100"])
347                .inc(),
348            _ => (),
349        };
350    }
351}
352
353impl<S, B> Transform<S, ServiceRequest> for PrometheusMetrics
354where
355    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError>,
356    B: MessageBody + 'static,
357    B::Error: Into<Box<dyn StdError + 'static>>,
358{
359    type Response = ServiceResponse<StreamMetrics<BoxBody>>;
360    type Error = ActixError;
361    type Transform = PrometheusMetricsMiddleware<S>;
362    type InitError = ();
363    type Future = Ready<Result<Self::Transform, Self::InitError>>;
364
365    fn new_transform(&self, service: S) -> Self::Future {
366        ready(Ok(PrometheusMetricsMiddleware {
367            service,
368            inner: Rc::new(self.clone()),
369        }))
370    }
371}
372
373pub struct PrometheusMetricsMiddleware<S> {
374    service: S,
375    inner: Rc<PrometheusMetrics>,
376}
377
378#[pin_project::pin_project]
379pub struct MetricsResponse<S, B>
380where
381    B: MessageBody,
382    S: Service<ServiceRequest>,
383{
384    #[pin]
385    fut: S::Future,
386    start: u64,
387    inner: Rc<PrometheusMetrics>,
388    _t: PhantomData<B>,
389}
390
391impl<S, B> Future for MetricsResponse<S, B>
392where
393    B: MessageBody + 'static,
394    B::Error: Into<Box<dyn StdError + 'static>>,
395    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError>,
396{
397    type Output = Result<ServiceResponse<StreamMetrics<BoxBody>>, ActixError>;
398
399    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
400        let this = self.project();
401
402        let start = *this.start;
403
404        let res = match ready!(this.fut.poll(cx)) {
405            Ok(res) => res,
406            Err(e) => return Poll::Ready(Err(e)),
407        };
408
409        let req = res.request();
410        let method = req.method().clone();
411        let pattern_or_path = req
412            .match_pattern()
413            .unwrap_or_else(|| req.path().to_string());
414        let path = req.path().to_string();
415        let inner = this.inner.clone();
416
417        Poll::Ready(Ok(res.map_body(move |mut head, body| {
418            // We short circuit the response status and body to serve the endpoint
419            // automagically. This way the user does not need to set the middleware *AND*
420            // an endpoint to serve middleware results. The user is only required to set
421            // the middleware and tell us what the endpoint should be.
422            if inner.matches(&path, &method) {
423                head.status = StatusCode::OK;
424                head.headers.insert(
425                    CONTENT_TYPE,
426                    HeaderValue::from_static("text/plain; version=0.0.4; charset=utf-8"),
427                );
428
429                let body = inner.metrics().boxed();
430
431                StreamMetrics {
432                    body,
433                    size: 0,
434                    start,
435                    inner,
436                    status: head.status,
437                    path: pattern_or_path,
438                    method,
439                }
440            } else {
441                let body = body.boxed();
442
443                StreamMetrics {
444                    body,
445                    size: 0,
446                    start,
447                    inner,
448                    status: head.status,
449                    path: pattern_or_path,
450                    method,
451                }
452            }
453        })))
454    }
455}
456
457impl<S, B> Service<ServiceRequest> for PrometheusMetricsMiddleware<S>
458where
459    B: MessageBody + 'static,
460    B::Error: Into<Box<dyn StdError + 'static>>,
461    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError>,
462{
463    type Response = ServiceResponse<StreamMetrics<BoxBody>>;
464    type Error = S::Error;
465    type Future = MetricsResponse<S, B>;
466
467    actix_service::forward_ready!(service);
468
469    fn call(&self, req: ServiceRequest) -> Self::Future {
470        MetricsResponse {
471            fut: self.service.call(req),
472            start: self.inner.clock.raw(),
473            inner: self.inner.clone(),
474            _t: PhantomData,
475        }
476    }
477}
478
479use actix_web::body::{BodySize, BoxBody, MessageBody};
480use actix_web::http::header::HeaderValue;
481use pin_project::{pin_project, pinned_drop};
482use std::collections::HashMap;
483use std::marker::PhantomData;
484
485#[doc(hidden)]
486#[pin_project(PinnedDrop)]
487pub struct StreamMetrics<B> {
488    #[pin]
489    body: B,
490    size: usize,
491    start: u64,
492    inner: Rc<PrometheusMetrics>,
493    status: StatusCode,
494    path: String,
495    method: Method,
496}
497
498#[pinned_drop]
499impl<B> PinnedDrop for StreamMetrics<B> {
500    fn drop(self: Pin<&mut Self>) {
501        // update the metrics for this request at the very end of responding
502        self.inner.update_metrics(
503            &self.path,
504            &self.method,
505            self.status,
506            self.start,
507            self.inner.clock.raw(),
508        );
509    }
510}
511
512impl<B> MessageBody for StreamMetrics<B>
513where
514    B: MessageBody,
515    B::Error: Into<ActixError>,
516{
517    type Error = ActixError;
518
519    fn size(&self) -> BodySize {
520        self.body.size()
521    }
522
523    fn poll_next(
524        self: Pin<&mut Self>,
525        cx: &mut Context<'_>,
526    ) -> Poll<Option<Result<Bytes, Self::Error>>> {
527        let this = self.project();
528
529        // TODO: MSRV 1.51: poll_map_err
530        match ready!(this.body.poll_next(cx)) {
531            Some(Ok(chunk)) => {
532                *this.size += chunk.len();
533                Poll::Ready(Some(Ok(chunk)))
534            }
535            Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
536            None => Poll::Ready(None),
537        }
538    }
539}
540
541// TODO: rework tests
542// #[cfg(test)]
543// mod tests {
544//     use super::*;
545//     use actix_web::rt as actix_rt;
546//     use actix_web::test::{call_service, init_service, read_body, read_response, TestRequest};
547//     use actix_web::{web, App, HttpResponse};
548//
549//     use actix_web::middleware::Compat;
550//     use prometheus::{Counter, Encoder, Opts, TextEncoder};
551//
552//     #[actix_rt::test]
553//     async fn middleware_basic() {
554//         let prometheus = PrometheusMetricsBuilder::new("actix_web_prom")
555//             .endpoint("/metrics")
556//             .build()
557//             .unwrap();
558//
559//         let mut app = init_service(
560//             App::new()
561//                 .wrap(prometheus)
562//                 .service(web::resource("/health_check").to(HttpResponse::Ok)),
563//         )
564//         .await;
565//
566//         let res = call_service(
567//             &mut app,
568//             TestRequest::with_uri("/health_check").to_request(),
569//         )
570//         .await;
571//         assert!(res.status().is_success());
572//         assert_eq!(read_body(res).await, "");
573//
574//         let res = call_service(&mut app, TestRequest::with_uri("/metrics").to_request()).await;
575//         assert_eq!(
576//             res.headers().get(CONTENT_TYPE).unwrap(),
577//             "text/plain; version=0.0.4; charset=utf-8"
578//         );
579//         let body = String::from_utf8(read_body(res).await.to_vec()).unwrap();
580//         println!("{:#?}", body);
581//         assert!(&body.contains(
582//             &String::from_utf8(web::Bytes::from(
583//                 "# HELP actix_web_prom_http_requests_duration_seconds HTTP request duration in seconds for all requests
584// # TYPE actix_web_prom_http_requests_duration_seconds histogram
585// actix_web_prom_http_requests_duration_seconds_bucket{endpoint=\"/health_check\",method=\"GET\",status=\"200\",le=\"0.005\"} 1
586// "
587//             ).to_vec()).unwrap()));
588//         assert!(body.contains(
589//             &String::from_utf8(
590//                 web::Bytes::from(
591//                     "# HELP actix_web_prom_http_requests_total Total number of HTTP requests
592// # TYPE actix_web_prom_http_requests_total counter
593// actix_web_prom_http_requests_total{endpoint=\"/health_check\",method=\"GET\",status=\"200\"} 1
594// "
595//                 )
596//                 .to_vec()
597//             )
598//             .unwrap()
599//         ));
600//     }
601//
602//     #[actix_rt::test]
603//     async fn middleware_scope() {
604//         let prometheus = PrometheusMetricsBuilder::new("actix_web_prom")
605//             .endpoint("/internal/metrics")
606//             .build()
607//             .unwrap();
608//
609//         let mut app = init_service(
610//             App::new().service(
611//                 web::scope("/internal")
612//                     .wrap(Compat::new(prometheus))
613//                     .service(web::resource("/health_check").to(HttpResponse::Ok)),
614//             ),
615//         )
616//         .await;
617//
618//         let res = call_service(
619//             &mut app,
620//             TestRequest::with_uri("/internal/health_check").to_request(),
621//         )
622//         .await;
623//         assert!(res.status().is_success());
624//         assert_eq!(read_body(res).await, "");
625//
626//         let res = call_service(
627//             &mut app,
628//             TestRequest::with_uri("/internal/metrics").to_request(),
629//         )
630//         .await;
631//         assert_eq!(
632//             res.headers().get(CONTENT_TYPE).unwrap(),
633//             "text/plain; version=0.0.4; charset=utf-8"
634//         );
635//         let body = String::from_utf8(read_body(res).await.to_vec()).unwrap();
636//         assert!(&body.contains(
637//             &String::from_utf8(web::Bytes::from(
638//                 "# HELP actix_web_prom_http_requests_duration_seconds HTTP request duration in seconds for all requests
639// # TYPE actix_web_prom_http_requests_duration_seconds histogram
640// actix_web_prom_http_requests_duration_seconds_bucket{endpoint=\"/internal/health_check\",method=\"GET\",status=\"200\",le=\"0.005\"} 1
641// "
642//             ).to_vec()).unwrap()));
643//         assert!(body.contains(
644//             &String::from_utf8(
645//                 web::Bytes::from(
646//                     "# HELP actix_web_prom_http_requests_total Total number of HTTP requests
647// # TYPE actix_web_prom_http_requests_total counter
648// actix_web_prom_http_requests_total{endpoint=\"/internal/health_check\",method=\"GET\",status=\"200\"} 1
649// "
650//                 )
651//                     .to_vec()
652//             )
653//                 .unwrap()
654//         ));
655//     }
656//
657//     #[actix_rt::test]
658//     async fn middleware_match_pattern() {
659//         let prometheus = PrometheusMetricsBuilder::new("actix_web_prom")
660//             .endpoint("/metrics")
661//             .build()
662//             .unwrap();
663//
664//         let mut app = init_service(
665//             App::new()
666//                 .wrap(prometheus)
667//                 .service(web::resource("/resource/{id}").to(HttpResponse::Ok)),
668//         )
669//         .await;
670//
671//         let res = call_service(
672//             &mut app,
673//             TestRequest::with_uri("/resource/123").to_request(),
674//         )
675//         .await;
676//         assert!(res.status().is_success());
677//         assert_eq!(read_body(res).await, "");
678//
679//         let res = read_response(&mut app, TestRequest::with_uri("/metrics").to_request()).await;
680//         let body = String::from_utf8(res.to_vec()).unwrap();
681//         assert!(&body.contains(
682//             &String::from_utf8(web::Bytes::from(
683//                 "# HELP actix_web_prom_http_requests_duration_seconds HTTP request duration in seconds for all requests
684// # TYPE actix_web_prom_http_requests_duration_seconds histogram
685// actix_web_prom_http_requests_duration_seconds_bucket{endpoint=\"/resource/{id}\",method=\"GET\",status=\"200\",le=\"0.005\"} 1
686// "
687//             ).to_vec()).unwrap()));
688//         assert!(body.contains(
689//             &String::from_utf8(
690//                 web::Bytes::from(
691//                     "# HELP actix_web_prom_http_requests_total Total number of HTTP requests
692// # TYPE actix_web_prom_http_requests_total counter
693// actix_web_prom_http_requests_total{endpoint=\"/resource/{id}\",method=\"GET\",status=\"200\"} 1
694// "
695//                 )
696//                 .to_vec()
697//             )
698//             .unwrap()
699//         ));
700//     }
701//
702//     #[actix_rt::test]
703//     async fn middleware_metrics_exposed_with_conflicting_pattern() {
704//         let prometheus = PrometheusMetricsBuilder::new("actix_web_prom")
705//             .endpoint("/metrics")
706//             .build()
707//             .unwrap();
708//
709//         let mut app = init_service(
710//             App::new()
711//                 .wrap(prometheus)
712//                 .service(web::resource("/{path}").to(HttpResponse::Ok)),
713//         )
714//         .await;
715//
716//         let res = call_service(&mut app, TestRequest::with_uri("/something").to_request()).await;
717//         assert!(res.status().is_success());
718//         assert_eq!(read_body(res).await, "");
719//
720//         let res = read_response(&mut app, TestRequest::with_uri("/metrics").to_request()).await;
721//         let body = String::from_utf8(res.to_vec()).unwrap();
722//         assert!(&body.contains(
723//             &String::from_utf8(web::Bytes::from(
724//                 "# HELP actix_web_prom_http_requests_duration_seconds HTTP request duration in seconds for all requests"
725//             ).to_vec()).unwrap()));
726//     }
727//
728//     #[actix_rt::test]
729//     async fn middleware_basic_failure() {
730//         let prometheus = PrometheusMetricsBuilder::new("actix_web_prom")
731//             .endpoint("/prometheus")
732//             .build()
733//             .unwrap();
734//
735//         let mut app = init_service(
736//             App::new()
737//                 .wrap(prometheus)
738//                 .service(web::resource("/health_check").to(HttpResponse::Ok)),
739//         )
740//         .await;
741//
742//         call_service(
743//             &mut app,
744//             TestRequest::with_uri("/health_checkz").to_request(),
745//         )
746//         .await;
747//         let res = read_response(&mut app, TestRequest::with_uri("/prometheus").to_request()).await;
748//         assert!(String::from_utf8(res.to_vec()).unwrap().contains(
749//             &String::from_utf8(
750//                 web::Bytes::from(
751//                     "# HELP actix_web_prom_http_requests_total Total number of HTTP requests
752// # TYPE actix_web_prom_http_requests_total counter
753// actix_web_prom_http_requests_total{endpoint=\"/health_checkz\",method=\"GET\",status=\"404\"} 1
754// "
755//                 )
756//                 .to_vec()
757//             )
758//             .unwrap()
759//         ));
760//     }
761//
762//     #[actix_rt::test]
763//     async fn middleware_custom_counter() {
764//         let counter_opts = Opts::new("counter", "some random counter").namespace("actix_web_prom");
765//         let counter = IntCounterVec::new(counter_opts, &["endpoint", "method", "status"]).unwrap();
766//
767//         let prometheus = PrometheusMetricsBuilder::new("actix_web_prom")
768//             .endpoint("/metrics")
769//             .build()
770//             .unwrap();
771//
772//         prometheus
773//             .registry
774//             .register(Box::new(counter.clone()))
775//             .unwrap();
776//
777//         let mut app = init_service(
778//             App::new()
779//                 .wrap(prometheus)
780//                 .service(web::resource("/health_check").to(HttpResponse::Ok)),
781//         )
782//         .await;
783//
784//         // Verify that 'counter' does not appear in the output before we use it
785//         call_service(
786//             &mut app,
787//             TestRequest::with_uri("/health_check").to_request(),
788//         )
789//         .await;
790//         let res = read_response(&mut app, TestRequest::with_uri("/metrics").to_request()).await;
791//         assert!(!String::from_utf8(res.to_vec()).unwrap().contains(
792//             &String::from_utf8(
793//                 web::Bytes::from(
794//                     "# HELP actix_web_prom_counter some random counter
795// # TYPE actix_web_prom_counter counter
796// actix_web_prom_counter{endpoint=\"endpoint\",method=\"method\",status=\"status\"} 1
797// "
798//                 )
799//                 .to_vec()
800//             )
801//             .unwrap()
802//         ));
803//
804//         // Verify that 'counter' appears after we use it
805//         counter
806//             .with_label_values(&["endpoint", "method", "status"])
807//             .inc();
808//         counter
809//             .with_label_values(&["endpoint", "method", "status"])
810//             .inc();
811//         call_service(&mut app, TestRequest::with_uri("/metrics").to_request()).await;
812//         let res = read_response(&mut app, TestRequest::with_uri("/metrics").to_request()).await;
813//         assert!(String::from_utf8(res.to_vec()).unwrap().contains(
814//             &String::from_utf8(
815//                 web::Bytes::from(
816//                     "# HELP actix_web_prom_counter some random counter
817// # TYPE actix_web_prom_counter counter
818// actix_web_prom_counter{endpoint=\"endpoint\",method=\"method\",status=\"status\"} 2
819// "
820//                 )
821//                 .to_vec()
822//             )
823//             .unwrap()
824//         ));
825//     }
826//
827//     #[actix_rt::test]
828//     async fn middleware_none_endpoint() {
829//         // Init PrometheusMetrics with none URL
830//         let prometheus = PrometheusMetricsBuilder::new("actix_web_prom")
831//             .build()
832//             .unwrap();
833//
834//         let mut app =
835//             init_service(App::new().wrap(prometheus.clone()).service(
836//                 web::resource("/metrics").to(|| HttpResponse::Ok().body("not prometheus")),
837//             ))
838//             .await;
839//
840//         let response =
841//             read_response(&mut app, TestRequest::with_uri("/metrics").to_request()).await;
842//
843//         // Assert app works
844//         assert_eq!(
845//             String::from_utf8(response.to_vec()).unwrap(),
846//             "not prometheus"
847//         );
848//
849//         // Assert counter counts
850//         let mut buffer = Vec::new();
851//         let encoder = TextEncoder::new();
852//         let metric_families = prometheus.registry.gather();
853//         encoder.encode(&metric_families, &mut buffer).unwrap();
854//         let output = String::from_utf8(buffer).unwrap();
855//
856//         assert!(output.contains(
857//             "actix_web_prom_http_requests_total{endpoint=\"/metrics\",method=\"GET\",status=\"200\"} 1"
858//         ));
859//     }
860//
861//     #[actix_rt::test]
862//     async fn middleware_custom_registry_works() {
863//         // Init Prometheus Registry
864//         let registry = Registry::new();
865//
866//         let counter_opts = Opts::new("test_counter", "test counter help");
867//         let counter = Counter::with_opts(counter_opts).unwrap();
868//         registry.register(Box::new(counter.clone())).unwrap();
869//
870//         counter.inc_by(10_f64);
871//
872//         // Init PrometheusMetrics
873//         let prometheus = PrometheusMetricsBuilder::new("actix_web_prom")
874//             .registry(registry)
875//             .endpoint("/metrics")
876//             .build()
877//             .unwrap();
878//
879//         let mut app = init_service(
880//             App::new()
881//                 .wrap(prometheus.clone())
882//                 .service(web::resource("/test").to(|| HttpResponse::Ok().finish())),
883//         )
884//         .await;
885//
886//         // all http counters are 0 because this is the first http request,
887//         // so we should get only 10 on test counter
888//         let response =
889//             read_response(&mut app, TestRequest::with_uri("/metrics").to_request()).await;
890//
891//         let ten_test_counter =
892//             "# HELP test_counter test counter help\n# TYPE test_counter counter\ntest_counter 10\n";
893//         assert_eq!(
894//             String::from_utf8(response.to_vec()).unwrap(),
895//             ten_test_counter
896//         );
897//
898//         // all http counters are 1 because this is the second http request,
899//         // plus 10 on test counter
900//         let response =
901//             read_response(&mut app, TestRequest::with_uri("/metrics").to_request()).await;
902//         let response_string = String::from_utf8(response.to_vec()).unwrap();
903//
904//         let one_http_counters = "# HELP actix_web_prom_http_requests_total Total number of HTTP requests\n# TYPE actix_web_prom_http_requests_total counter\nactix_web_prom_http_requests_total{endpoint=\"/metrics\",method=\"GET\",status=\"200\"} 1";
905//
906//         assert!(response_string.contains(ten_test_counter));
907//         assert!(response_string.contains(one_http_counters));
908//     }
909//
910//     #[actix_rt::test]
911//     async fn middleware_const_labels() {
912//         let mut labels = HashMap::new();
913//         labels.insert("label1".to_string(), "value1".to_string());
914//         labels.insert("label2".to_string(), "value2".to_string());
915//         let prometheus = PrometheusMetricsBuilder::new("actix_web_prom")
916//             .endpoint("/metrics")
917//             .const_labels(labels)
918//             .build()
919//             .unwrap();
920//
921//         let mut app = init_service(
922//             App::new()
923//                 .wrap(prometheus)
924//                 .service(web::resource("/health_check").to(HttpResponse::Ok)),
925//         )
926//         .await;
927//
928//         let res = call_service(
929//             &mut app,
930//             TestRequest::with_uri("/health_check").to_request(),
931//         )
932//         .await;
933//         assert!(res.status().is_success());
934//         assert_eq!(read_body(res).await, "");
935//
936//         let res = read_response(&mut app, TestRequest::with_uri("/metrics").to_request()).await;
937//         let body = String::from_utf8(res.to_vec()).unwrap();
938//         assert!(&body.contains(
939//             &String::from_utf8(web::Bytes::from(
940//                 "# HELP actix_web_prom_http_requests_duration_seconds HTTP request duration in seconds for all requests
941// # TYPE actix_web_prom_http_requests_duration_seconds histogram
942// actix_web_prom_http_requests_duration_seconds_bucket{endpoint=\"/health_check\",label1=\"value1\",label2=\"value2\",method=\"GET\",status=\"200\",le=\"0.005\"} 1
943// "
944//             ).to_vec()).unwrap()));
945//         assert!(body.contains(
946//             &String::from_utf8(
947//                 web::Bytes::from(
948//                     "# HELP actix_web_prom_http_requests_total Total number of HTTP requests
949// # TYPE actix_web_prom_http_requests_total counter
950// actix_web_prom_http_requests_total{endpoint=\"/health_check\",label1=\"value1\",label2=\"value2\",method=\"GET\",status=\"200\"} 1
951// "
952//                 )
953//                     .to_vec()
954//             )
955//                 .unwrap()
956//         ));
957//     }
958// }