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// }