Prometheus instrumentation for actix-web. This middleware is heavily influenced by the work in sd2k/rocket_prometheus. We track the same default metrics and allow for adding user defined metrics.
By default two metrics are tracked (this assumes the namespace actix_web_prom
):
-
actix_web_prom_http_requests_total
(labels: endpoint, method, status): the total number of HTTP requests handled by the actix HttpServer. -
actix_web_prom_http_requests_duration_seconds
(labels: endpoint, method, status): the request duration for all HTTP requests handled by the actix HttpServer.
First add actix-web-prom
to your Cargo.toml
:
[dependencies]
actix-web-prom = "0.5"
You then instantiate the prometheus middleware and pass it to .wrap()
:
use std::collections::HashMap;
use actix_web::{web, App, HttpResponse, HttpServer};
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};
fn health() -> HttpResponse {
HttpResponse::Ok().finish()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let mut labels = HashMap::new();
labels.insert("label1".to_string(), "value1".to_string());
let prometheus = PrometheusMetricsBuilder::new("api")
.endpoint("/metrics")
.const_labels(labels)
.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(())
}
Using the above as an example, a few things are worth mentioning:
api
is the metrics namespace/metrics
will be auto exposed (GET requests only) with Content-Type headercontent-type: text/plain; version=0.0.4; charset=utf-8
Some(labels)
is used to add fixed labels to the metrics;None
can be passed instead if no additional labels are necessary.
A call to the /metrics endpoint will expose your metrics:
$ curl http://localhost:8080/metrics
# HELP api_http_requests_duration_seconds HTTP request duration in seconds for all requests
# TYPE api_http_requests_duration_seconds histogram
api_http_requests_duration_seconds_bucket{endpoint="/metrics",label1="value1",method="GET",status="200",le="0.005"} 1
api_http_requests_duration_seconds_bucket{endpoint="/metrics",label1="value1",method="GET",status="200",le="0.01"} 1
api_http_requests_duration_seconds_bucket{endpoint="/metrics",label1="value1",method="GET",status="200",le="0.025"} 1
api_http_requests_duration_seconds_bucket{endpoint="/metrics",label1="value1",method="GET",status="200",le="0.05"} 1
api_http_requests_duration_seconds_bucket{endpoint="/metrics",label1="value1",method="GET",status="200",le="0.1"} 1
api_http_requests_duration_seconds_bucket{endpoint="/metrics",label1="value1",method="GET",status="200",le="0.25"} 1
api_http_requests_duration_seconds_bucket{endpoint="/metrics",label1="value1",method="GET",status="200",le="0.5"} 1
api_http_requests_duration_seconds_bucket{endpoint="/metrics",label1="value1",method="GET",status="200",le="1"} 1
api_http_requests_duration_seconds_bucket{endpoint="/metrics",label1="value1",method="GET",status="200",le="2.5"} 1
api_http_requests_duration_seconds_bucket{endpoint="/metrics",label1="value1",method="GET",status="200",le="5"} 1
api_http_requests_duration_seconds_bucket{endpoint="/metrics",label1="value1",method="GET",status="200",le="10"} 1
api_http_requests_duration_seconds_bucket{endpoint="/metrics",label1="value1",method="GET",status="200",le="+Inf"} 1
api_http_requests_duration_seconds_sum{endpoint="/metrics",label1="value1",method="GET",status="200"} 0.00003
api_http_requests_duration_seconds_count{endpoint="/metrics",label1="value1",method="GET",status="200"} 1
# HELP api_http_requests_total Total number of HTTP requests
# TYPE api_http_requests_total counter
api_http_requests_total{endpoint="/metrics",label1="value1",method="GET",status="200"} 1
You instantiate PrometheusMetrics
and then use its .registry
to register your custom
metric (in this case, we use a IntCounterVec
).
Then you can pass this counter through .data()
to have it available within the resource
responder.
use actix_web::{web, App, HttpResponse, HttpServer};
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};
use prometheus::{opts, IntCounterVec};
fn health(counter: web::Data<IntCounterVec>) -> HttpResponse {
counter.with_label_values(&["endpoint", "method", "status"]).inc();
HttpResponse::Ok().finish()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let prometheus = PrometheusMetricsBuilder::new("api")
.endpoint("/metrics")
.build()
.unwrap();
let counter_opts = opts!("counter", "some random counter").namespace("api");
let counter = IntCounterVec::new(counter_opts, &["endpoint", "method", "status"]).unwrap();
prometheus
.registry
.register(Box::new(counter.clone()))
.unwrap();
HttpServer::new(move || {
App::new()
.wrap(prometheus.clone())
.data(counter.clone())
.service(web::resource("/health").to(health))
})
.bind("127.0.0.1:8080")?
.run()
.await?;
Ok(())
}
Some apps might have more than one actix_web::HttpServer
.
If that's the case, you might want to use your own registry:
use actix_web::{web, App, HttpResponse, HttpServer};
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};
use actix_web::rt::System;
use prometheus::Registry;
use std::thread;
fn public_handler() -> HttpResponse {
HttpResponse::Ok().body("Everyone can see it!")
}
fn private_handler() -> HttpResponse {
HttpResponse::Ok().body("This can be hidden behind a firewall")
}
fn main() -> std::io::Result<()> {
let shared_registry = Registry::new();
let private_metrics = PrometheusMetricsBuilder::new("private_api")
.registry(shared_registry.clone())
.endpoint("/metrics")
.build()
// It is safe to unwrap when __no other app has the same namespace__
.unwrap();
let public_metrics = PrometheusMetricsBuilder::new("public_api")
.registry(shared_registry.clone())
// Metrics should not be available from the outside
// so no endpoint is registered
.build()
.unwrap();
let private_thread = thread::spawn(move || {
let mut sys = System::new("private");
let srv = HttpServer::new(move || {
App::new()
.wrap(private_metrics.clone())
.service(web::resource("/test").to(private_handler))
})
.bind("127.0.0.1:8081")
.unwrap()
.run();
sys.block_on(srv).unwrap();
});
let public_thread = thread::spawn(|| {
let mut sys = System::new("public");
let srv = HttpServer::new(move || {
App::new()
.wrap(public_metrics.clone())
.service(web::resource("/test").to(public_handler))
})
.bind("127.0.0.1:8082")
.unwrap()
.run();
sys.block_on(srv).unwrap();
});
private_thread.join().unwrap();
public_thread.join().unwrap();
Ok(())
}