diff --git a/README.md b/README.md index 4133b31..b2696a7 100644 --- a/README.md +++ b/README.md @@ -232,3 +232,20 @@ web::resource("/posts/{language}/{slug}") See the full example `with_cardinality_on_params.rs`. +### Configurable metric names + +If you want to rename the default metrics, you can use `ActixMetricsConfiguration` to do so. + +```rust +use actix_web_prom::{PrometheusMetricsBuilder, ActixMetricsConfiguration}; + +PrometheusMetricsBuilder::new("api") + .endpoint("/metrics") + .metrics_configuration( + ActixMetricsConfiguration::default() + .http_requests_duration_seconds_name("my_http_request_duration"), + ) + .build() + .unwrap(); +``` +See full example `confuring_default_metrics.rs`. diff --git a/examples/configuring_default_metrics.rs b/examples/configuring_default_metrics.rs new file mode 100644 index 0000000..536fa73 --- /dev/null +++ b/examples/configuring_default_metrics.rs @@ -0,0 +1,28 @@ +use actix_web::{web, App, HttpResponse, HttpServer}; +use actix_web_prom::{ActixMetricsConfiguration, PrometheusMetricsBuilder}; + +async fn health() -> HttpResponse { + HttpResponse::Ok().finish() +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let prometheus = PrometheusMetricsBuilder::new("api") + .endpoint("/metrics") + .metrics_configuration( + ActixMetricsConfiguration::default() + .http_requests_duration_seconds_name("my_http_request_duration"), + ) + .build() + .unwrap(); + + HttpServer::new(move || { + App::new() + .wrap(prometheus.clone()) + .service(web::resource("/health").to(health)) + }) + .bind("127.0.0.1:8080")? + .run() + .await?; + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 662b99c..7410985 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -283,7 +283,7 @@ pub struct PrometheusMetricsBuilder { exclude: HashSet, exclude_regex: RegexSet, exclude_status: HashSet, - enable_http_version_label: bool, + metrics_configuration: ActixMetricsConfiguration, } impl PrometheusMetricsBuilder { @@ -300,7 +300,7 @@ impl PrometheusMetricsBuilder { exclude: HashSet::new(), exclude_regex: RegexSet::empty(), exclude_status: HashSet::new(), - enable_http_version_label: false, + metrics_configuration: ActixMetricsConfiguration::default(), } } @@ -352,29 +352,32 @@ impl PrometheusMetricsBuilder { self } - /// Report HTTP version of served request as "version" label - pub fn enable_http_version_label(mut self, enable: bool) -> Self { - self.enable_http_version_label = enable; + /// Set metrics configuration + pub fn metrics_configuration(mut self, value: ActixMetricsConfiguration) -> Self { + self.metrics_configuration = value; self } /// Instantiate PrometheusMetrics struct pub fn build(self) -> Result> { - let http_requests_total_opts = - Opts::new("http_requests_total", "Total number of HTTP requests") - .namespace(&self.namespace) - .const_labels(self.const_labels.clone()); - - let label_names = if self.enable_http_version_label { - ["version", "endpoint", "method", "status"].as_slice() - } else { - ["endpoint", "method", "status"].as_slice() - }; + let labels_vec = self.metrics_configuration.labels.clone().to_vec(); + let labels = &labels_vec.iter().map(|s| s.as_str()).collect::>(); + + let http_requests_total_opts = Opts::new( + self.metrics_configuration + .http_requests_total_name + .to_owned(), + "Total number of HTTP requests", + ) + .namespace(&self.namespace) + .const_labels(self.const_labels.clone()); - let http_requests_total = IntCounterVec::new(http_requests_total_opts, label_names)?; + let http_requests_total = IntCounterVec::new(http_requests_total_opts, labels)?; let http_requests_duration_seconds_opts = HistogramOpts::new( - "http_requests_duration_seconds", + self.metrics_configuration + .http_requests_duration_seconds_name + .to_owned(), "HTTP request duration in seconds for all requests", ) .namespace(&self.namespace) @@ -382,7 +385,7 @@ impl PrometheusMetricsBuilder { .const_labels(self.const_labels.clone()); let http_requests_duration_seconds = - HistogramVec::new(http_requests_duration_seconds_opts, label_names)?; + HistogramVec::new(http_requests_duration_seconds_opts, labels)?; self.registry .register(Box::new(http_requests_total.clone()))?; @@ -399,11 +402,103 @@ impl PrometheusMetricsBuilder { exclude: self.exclude, exclude_regex: self.exclude_regex, exclude_status: self.exclude_status, - enable_http_version_label: self.enable_http_version_label, + enable_http_version_label: self.metrics_configuration.labels.version.is_some(), }) } } +#[derive(Debug, Clone)] +///Configurations for the labels used in metrics +pub struct LabelsConfiguration { + endpoint: String, + method: String, + status: String, + version: Option, +} + +impl LabelsConfiguration { + /// construct default LabelsConfiguration + pub fn default() -> LabelsConfiguration { + LabelsConfiguration { + endpoint: String::from("endpoint"), + method: String::from("method"), + status: String::from("status"), + version: None, + } + } + + fn to_vec(self) -> Vec { + let mut labels = vec![self.endpoint, self.method, self.status]; + if let Some(version) = self.version { + labels.push(version); + } + labels + } + + /// set http method label + pub fn method(mut self, name: &str) -> Self { + self.method = name.to_owned(); + self + } + + /// set http endpoint label + pub fn endpoint(mut self, name: &str) -> Self { + self.endpoint = name.to_owned(); + self + } + + /// set http status label + pub fn status(mut self, name: &str) -> Self { + self.status = name.to_owned(); + self + } + + /// set http version label + pub fn version(mut self, name: &str) -> Self { + self.version = Some(name.to_owned()); + self + } +} + +#[derive(Debug, Clone)] +/// Configuration for the collected metrics +/// +/// Stores individual metric configuration objects +pub struct ActixMetricsConfiguration { + http_requests_total_name: String, + http_requests_duration_seconds_name: String, + labels: LabelsConfiguration, +} + +impl ActixMetricsConfiguration { + /// Create the default metrics configuration + pub fn default() -> ActixMetricsConfiguration { + ActixMetricsConfiguration { + http_requests_total_name: String::from("http_requests_total"), + http_requests_duration_seconds_name: String::from("http_requests_duration_seconds"), + labels: LabelsConfiguration::default(), + } + } + + /// Set the labels collected for the metrics + pub fn labels(mut self, labels: LabelsConfiguration) -> Self { + self.labels = labels; + self + } + + /// Set name for http_requests_total metric + pub fn http_requests_total_name(mut self, name: &str) -> Self { + self.http_requests_total_name = name.to_owned(); + self + } + + /// Set name for http_requests_duration_seconds metric + pub fn http_requests_duration_seconds_name(mut self, name: &str) -> Self { + self.http_requests_duration_seconds_name = name.to_owned(); + self + } +} + #[derive(Clone)] #[must_use = "must be set up as middleware for actix-web"] /// By default two metrics are tracked (this assumes the namespace `actix_web_prom`): @@ -485,15 +580,15 @@ impl PrometheusMetrics { }; let label_values = [ - Self::http_version_label(http_version), final_pattern, method.as_str(), status.as_str(), + Self::http_version_label(http_version), ]; let label_values = if self.enable_http_version_label { &label_values[..] } else { - &label_values[1..] + &label_values[..3] }; let elapsed = clock.elapsed(); @@ -782,7 +877,10 @@ actix_web_prom_http_requests_total{endpoint=\"/health_check\",method=\"GET\",sta async fn middleware_http_version() { let prometheus = PrometheusMetricsBuilder::new("actix_web_prom") .endpoint("/metrics") - .enable_http_version_label(true) + .metrics_configuration( + ActixMetricsConfiguration::default() + .labels(LabelsConfiguration::default().version("version")), + ) .build() .unwrap(); @@ -1254,6 +1352,52 @@ actix_web_prom_http_requests_total{endpoint=\"/health_check\",label1=\"value1\", )); } + #[actix_web::test] + async fn middleware_metrics_configuration() { + let metrics_config = ActixMetricsConfiguration::default() + .http_requests_duration_seconds_name("my_http_request_duration") + .http_requests_total_name("my_http_requests_total"); + + let prometheus = PrometheusMetricsBuilder::new("actix_web_prom") + .endpoint("/metrics") + .metrics_configuration(metrics_config) + .build() + .unwrap(); + + let app = init_service( + App::new() + .wrap(prometheus) + .service(web::resource("/health_check").to(HttpResponse::Ok)), + ) + .await; + + let res = call_service(&app, TestRequest::with_uri("/health_check").to_request()).await; + assert!(res.status().is_success()); + assert_eq!(read_body(res).await, ""); + + let res = call_and_read_body(&app, TestRequest::with_uri("/metrics").to_request()).await; + let body = String::from_utf8(res.to_vec()).unwrap(); + assert!(&body.contains( + &String::from_utf8(web::Bytes::from( + "# HELP actix_web_prom_my_http_request_duration HTTP request duration in seconds for all requests +# TYPE actix_web_prom_my_http_request_duration histogram +actix_web_prom_my_http_request_duration_bucket{endpoint=\"/health_check\",method=\"GET\",status=\"200\",le=\"0.005\"} 1 +" + ).to_vec()).unwrap())); + assert!(body.contains( + &String::from_utf8( + web::Bytes::from( + "# HELP actix_web_prom_my_http_requests_total Total number of HTTP requests +# TYPE actix_web_prom_my_http_requests_total counter +actix_web_prom_my_http_requests_total{endpoint=\"/health_check\",method=\"GET\",status=\"200\"} 1 +" + ) + .to_vec() + ) + .unwrap() + )); + } + #[test] fn compat_with_non_boxed_middleware() { let _app = App::new()