An open API service indexing awesome lists of open source software.

https://github.com/nlopes/actix-web-prom

Actix-web middleware to expose Prometheus metrics
https://github.com/nlopes/actix-web-prom

actix-web middleware prometheus prometheus-metrics

Last synced: about 1 year ago
JSON representation

Actix-web middleware to expose Prometheus metrics

Awesome Lists containing this project

README

          

# actix-web-prom

[![CI Status](https://github.com/nlopes/actix-web-prom/workflows/Test/badge.svg)](https://github.com/nlopes/actix-web-prom/actions)
[![docs.rs](https://docs.rs/actix-web-prom/badge.svg)](https://docs.rs/actix-web-prom)
[![crates.io](https://img.shields.io/crates/v/actix-web-prom.svg)](https://crates.io/crates/actix-web-prom)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nlopes/actix-web-prom/blob/master/LICENSE)

Prometheus instrumentation for [actix-web](https://github.com/actix/actix-web). This middleware is heavily influenced by the work in [sd2k/rocket_prometheus](https://github.com/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.

## Usage

First add `actix-web-prom` to your `Cargo.toml`:

```toml
[dependencies]
actix-web-prom = "0.10.0"
```

You then instantiate the prometheus middleware and pass it to `.wrap()`:

```rust
use std::collections::HashMap;

use actix_web::{web, App, HttpResponse, HttpServer};
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};

async 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 header `content-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:

```shell
$ 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
```

### Features
If you enable `process` feature of this crate, default process metrics will also be collected.
[Default process metrics](https://prometheus.io/docs/instrumenting/writing_clientlibs/#process-metrics)

```shell
# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE process_cpu_seconds_total counter
process_cpu_seconds_total 0.22
# HELP process_max_fds Maximum number of open file descriptors.
# TYPE process_max_fds gauge
process_max_fds 1048576
# HELP process_open_fds Number of open file descriptors.
# TYPE process_open_fds gauge
process_open_fds 78
# HELP process_resident_memory_bytes Resident memory size in bytes.
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 17526784
# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1628105774.92
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 1893163008
```

### Custom metrics

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.

```rust
use actix_web::{web, App, HttpResponse, HttpServer};
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};
use prometheus::{opts, IntCounterVec};

async fn health(counter: web::Data) -> 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(())
}
```

### Custom `Registry`

Some apps might have more than one `actix_web::HttpServer`.
If that's the case, you might want to use your own registry:

```rust
use actix_web::{web, App, HttpResponse, HttpServer};
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};
use actix_web::rt::System;
use prometheus::Registry;
use std::thread;

async fn public_handler() -> HttpResponse {
HttpResponse::Ok().body("Everyone can see it!")
}

async 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();
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();
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(())
}

```

### Configurable routes pattern cardinality

Let's say you have on your app a route to fetch posts by language and by slug `GET /posts/{language}/{slug}`.
By default, actix-web-prom will provide metrics for the whole route with the label `endpoint` set to the pattern `/posts/{language}/{slug}`.
This is great but you cannot differentiate metrics across languages (as there is only a limited set of them).
Actix-web-prom can be configured to allow for more cardinality on some route params.

For that you need to add a middleware to pass some [extensions data](https://blog.adamchalmers.com/what-are-extensions/), specifically the `MetricsConfig` struct that contains the list of params you want to keep cardinality on.

```rust
use actix_web::{dev::Service, web, HttpMessage, HttpResponse};
use actix_web_prom::MetricsConfig;

async fn handler() -> HttpResponse {
HttpResponse::Ok().finish()
}

web::resource("/posts/{language}/{slug}")
.wrap_fn(|req, srv| {
req.extensions_mut().insert::(
MetricsConfig { cardinality_keep_params: vec!["language".to_string()] }
);
srv.call(req)
})
.route(web::get().to(handler));
```

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 `configuring_default_metrics.rs`.

### Masking unknown paths

This is useful to avoid producting lots and lots of useless metrics due to bots on the internet.

What this does is transform a path that will never be found (404) into *one single
metric*. So, if you want metrics about every single path that is hit, even if it doesn't
exist, avoid this section altogether.

```rust
use actix_web_prom::PrometheusMetricsBuilder;

PrometheusMetricsBuilder::new("api")
.endpoint("/metrics")
.mask_unmatched_patterns("UNKNOWN")
.build()
.unwrap();
```

The above will convert all `/` into `UNKNOWN`:

```
http_requests_duration_seconds_sum{endpoint="/favicon.ico",method="GET",status="400"} 0.000424898
```

becomes

```
http_requests_duration_seconds_sum{endpoint="UNKNOWN",method="GET",status="400"} 0.000424898
```