[`metrics`] + [`prometheus`] = ❤️
=================================
[![crates.io](https://img.shields.io/crates/v/metrics-prometheus.svg "crates.io")](https://crates.io/crates/metrics-prometheus)
[![Rust 1.81+](https://img.shields.io/badge/rustc-1.81+-lightgray.svg "Rust 1.81+")](https://blog.rust-lang.org/2024/09/05/Rust-1.81.0.html)
[![Unsafe Forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg "Unsafe forbidden")](https://github.com/rust-secure-code/safety-dance)
[![CI](https://github.com/instrumentisto/metrics-prometheus-rs/workflows/CI/badge.svg?branch=main "CI")](https://github.com/instrumentisto/metrics-prometheus-rs/actions?query=workflow%3ACI+branch%3Amain)
[![Rust docs](https://docs.rs/metrics-prometheus/badge.svg "Rust docs")](https://docs.rs/metrics-prometheus)
[API Docs](https://docs.rs/metrics-prometheus) |
[Changelog](https://github.com/instrumentisto/metrics-prometheus-rs/blob/main/CHANGELOG.md)
[`prometheus`] backend for [`metrics`] crate.
## Motivation
[Rust] has at least two ecosystems regarding metrics collection:
- One is based on the [`prometheus`] crate, focusing on delivering metrics to [Prometheus] (or its drop-in replacements like [VictoriaMetrics]). It provides a lot of [Prometheus]-specific capabilities and validates metrics strictly to meet the format used by [Prometheus].
- Another one is based on the [`metrics`] crate, being more generic and targeting a wider scope, rather than [Prometheus] only. It provides a convenient and ergonomic facade, allowing to work with metrics in the very similar way we do work with logs and traces via [`log`]/[`tracing`] ecosystems (and even [supports `tracing::Span`s for metrics labels][`metrics-tracing-context`]).
As the result, some crates use [`prometheus`] crate for providing their metrics, and another crates do use [`metrics`] crate for that. Furthermore, [`prometheus`] and [`metrics`] crates are designed quite differently, making their composition a non-trivial task. This crate aims to mitigate this gap, allowing to combine both [`prometheus`] and [`metrics`] ecosystems in a single project.
### Alternatives
If you're not obligated to deal with [`prometheus`] crate directly or via third-party crates which do use it, consider the [`metrics-exporter-prometheus`] crate, which provides a simple [Prometheus] backend for [`metrics`] facade, without bringing in the whole [`prometheus`] crate's machinery.
## Overview
This crate provides a [`metrics::Recorder`] implementation, allowing to work with a [`prometheus::Registry`] via [`metrics`] facade.
It comes in 3 flavours, allowing to choose the smallest performance overhead depending on a use case:
- Regular [`Recorder`], allowing to create new metrics via [`metrics`] facade anytime, without limits. Provides the same overhead of accessing an already registered metric as a [`metrics::Registry`] does: [`read`-lock] on a sharded [`HashMap`] plus [`Arc`] cloning.
- [`FrozenRecorder`], unable to create new metrics via [`metrics`] facade at all (just no-op in such case). Provides the smallest overhead of accessing an already registered metric: just a regular [`HashMap`] lookup plus [`Arc`] cloning.
- [`FreezableRecorder`], acting the same way as the [`Recorder`] at first, but being able to [`.freeze()`] and so, becoming a [`FrozenRecorder`] at the end. The overhead of accessing an already registered metric is the same as [`Recorder`] and [`FrozenRecorder`] provide, plus [`AtomicBool`] loading to check whether it has been [`.freeze()`]d.
Not any [`prometheus`] metric is supported, because [`metrics`] crate implies only few of them. This is how the [`metrics`] crate's metrics are mapped onto [`prometheus`] ones:
- [`metrics::Counter`]: [`prometheus::IntCounter`] + [`prometheus::IntCounterVec`]
- [`metrics::Gauge`]: [`prometheus::Gauge`] + [`prometheus::GaugeVec`]
- [`metrics::Histogram`]: [`prometheus::Histogram`] + [`prometheus::HistogramVec`]
[`prometheus::MetricVec`] types are used whenever any labels are specified via [`metrics`] facade.
To satisfy the [`metrics::Recorder`]'s requirement of allowing changing metrics description anytime after its registration ([`prometheus`] crate doesn't imply and allow that), the [`Describable`] wrapper is used, allowing to [`arc-swap`] the description.
```rust
// By default `prometheus::default_registry()` is used.
let recorder = metrics_prometheus::install();
// Either use `metrics` crate interfaces.
metrics::counter!("count", "whose" => "mine", "kind" => "owned").increment(1);
metrics::counter!("count", "whose" => "mine", "kind" => "ref").increment(1);
metrics::counter!("count", "kind" => "owned", "whose" => "dummy").increment(1);
// Or construct and provide `prometheus` metrics directly.
recorder.register_metric(prometheus::Gauge::new("value", "help")?);
let report = prometheus::TextEncoder::new()
.encode_to_string(&prometheus::default_registry().gather())?;
assert_eq!(
report.trim(),
r#"
## HELP count count
## TYPE count counter
count{kind="owned",whose="dummy"} 1
count{kind="owned",whose="mine"} 1
count{kind="ref",whose="mine"} 1
## HELP value help
## TYPE value gauge
value 0
"#
.trim(),
);
// Metrics can be described anytime after being registered in
// `prometheus::Registry`.
metrics::describe_counter!("count", "Example of counter.");
metrics::describe_gauge!("value", "Example of gauge.");
let report = prometheus::TextEncoder::new()
.encode_to_string(&recorder.registry().gather())?;
assert_eq!(
report.trim(),
r#"
## HELP count Example of counter.
## TYPE count counter
count{kind="owned",whose="dummy"} 1
count{kind="owned",whose="mine"} 1
count{kind="ref",whose="mine"} 1
## HELP value Example of gauge.
## TYPE value gauge
value 0
"#
.trim(),
);
// Description can be changed multiple times and anytime.
metrics::describe_counter!("count", "Another description.");
// Even before a metric is registered in `prometheus::Registry`.
metrics::describe_counter!("another", "Yet another counter.");
metrics::counter!("another").increment(1);
let report = prometheus::TextEncoder::new()
.encode_to_string(&recorder.registry().gather())?;
assert_eq!(
report.trim(),
r#"
## HELP another Yet another counter.
## TYPE another counter
another 1
## HELP count Another description.
## TYPE count counter
count{kind="owned",whose="dummy"} 1
count{kind="owned",whose="mine"} 1
count{kind="ref",whose="mine"} 1
## HELP value Example of gauge.
## TYPE value gauge
value 0
"#
.trim(),
);
# Ok::<_, prometheus::Error>(())
```
### Limitations
Since [`prometheus`] crate validates the metrics format very strictly, not everything, expressed via [`metrics`] facade, may be put into a [`prometheus::Registry`], ending up with a [`prometheus::Error`] being emitted.
- Metric names cannot be namespaced with dots (and should follow [Prometheus] format).
```rust,should_panic
metrics_prometheus::install();
// panics: 'queries.count' is not a valid metric name
metrics::counter!("queries.count").increment(1);
```
- The same metric should use always the same set of labels:
```rust,should_panic
metrics_prometheus::install();
metrics::counter!("count").increment(1);
// panics: Inconsistent label cardinality, expect 0 label values, but got 1
metrics::counter!("count", "whose" => "mine").increment(1);
```
```rust,should_panic
metrics_prometheus::install();
metrics::counter!("count", "kind" => "owned").increment(1);
// panics: label name kind missing in label map
metrics::counter!("count", "whose" => "mine").increment(1);
```
```rust,should_panic
metrics_prometheus::install();
metrics::counter!("count", "kind" => "owned").increment(1);
// panics: Inconsistent label cardinality, expect 1 label values, but got 2
metrics::counter!("count", "kind" => "ref", "whose" => "mine").increment(1);
```
- The same name cannot be used for different types of metrics:
```rust,should_panic
metrics_prometheus::install();
metrics::counter!("count").increment(1);
// panics: Duplicate metrics collector registration attempted
metrics::gauge!("count").increment(1.0);
```
- Any metric registered in a [`prometheus::Registry`] directly, without using [`metrics`] or this crate interfaces, is not usable via [`metrics`] facade and will cause a [`prometheus::Error`].
```rust,should_panic
metrics_prometheus::install();
prometheus::default_registry()
.register(Box::new(prometheus::Gauge::new("value", "help")?))?;
// panics: Duplicate metrics collector registration attempted
metrics::gauge!("value").increment(4.5);
# Ok::<_, prometheus::Error>(())
```
- [`metrics::Unit`]s are not supported, as [Prometheus] has no notion of ones. Specifying them via [`metrics`] macros will be no-op.
### [`prometheus::Error`] handling
Since [`metrics::Recorder`] doesn't expose any errors in its API, the emitted [`prometheus::Error`]s can be either turned into a panic, or just silently ignored, returning a no-op metric instead (see [`metrics::Counter::noop()`] for example).
This can be tuned by providing a [`failure::Strategy`] when building a [`Recorder`].
```rust
use metrics_prometheus::failure::strategy;
metrics_prometheus::Recorder::builder()
.with_failure_strategy(strategy::NoOp)
.build_and_install();
// `prometheus::Error` is ignored inside.
metrics::counter!("invalid.name").increment(1);
let stats = prometheus::default_registry().gather();
assert_eq!(stats.len(), 0);
```
The default [`failure::Strategy`] is [`PanicInDebugNoOpInRelease`]. See [`failure::strategy`] module for other available [`failure::Strategy`]s,
or provide your own one by implementing the [`failure::Strategy`] trait.
## License
Copyright © 2022-2024 Instrumentisto Team,
Licensed under either of [Apache License, Version 2.0][APACHE] or [MIT license][MIT] at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the [Apache-2.0 license][APACHE], shall be dual licensed as above, without any additional terms or conditions.
[`.freeze()`]: https://docs.rs/metrics-prometheus/latest/metrics_prometheus/struct.FreezableRecorder.html#method.freeze
[`Arc`]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html
[`arc-swap`]: https://docs.rs/arc-swap
[`AtomicBool`]: https://doc.rust-lang.org/stable/std/sync/atomic/struct.AtomicBool.html
[`Describable`]: https://docs.rs/metrics-prometheus/latest/metrics_prometheus/metric/struct.Describable.html
[`failure::strategy`]: https://docs.rs/metrics-prometheus/latest/metrics_prometheus/failure/strategy/index.html
[`failure::Strategy`]: https://docs.rs/metrics-prometheus/latest/metrics_prometheus/failure/trait.Strategy.html
[`FreezableRecorder`]: https://docs.rs/metrics-prometheus/latest/metrics_prometheus/struct.FreezableRecorder.html
[`FrozenRecorder`]: https://docs.rs/metrics-prometheus/latest/metrics_prometheus/struct.FrozenRecorder.html
[`HashMap`]: https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html
[`log`]: https://docs.rs/log
[`metrics`]: https://docs.rs/metrics
[`metrics::Counter`]: https://docs.rs/metrics/latest/metrics/struct.Counter.html
[`metrics::Counter::noop()`]: https://docs.rs/metrics/latest/metrics/struct.Counter.html#method.noop
[`metrics::Gauge`]: https://docs.rs/metrics/latest/metrics/struct.Gauge.html
[`metrics::Histogram`]: https://docs.rs/metrics/latest/metrics/struct.Histogram.html
[`metrics::Recorder`]: https://docs.rs/metrics/latest/metrics/trait.Recorder.html
[`metrics::Registry`]: https://docs.rs/metrics-util/latest/metrics_util/registry/struct.Registry.html
[`metrics::Unit`]: https://docs.rs/metrics/latest/metrics/enum.Unit.html
[`metrics-exporter-prometheus`]: https://docs.rs/metrics-exporter-prometheus
[`metrics-tracing-context`]: https://docs.rs/metrics-tracing-context
[`PanicInDebugNoOpInRelease`]: https://docs.rs/metrics-prometheus/latest/metrics_prometheus/failure/strategy/struct.PanicInDebugNoOpInRelease.html
[`prometheus`]: https://docs.rs/prometheus
[`prometheus::Error`]: https://docs.rs/prometheus/latest/prometheus/enum.Error.html
[`prometheus::Gauge`]: https://docs.rs/prometheus/latest/prometheus/type.Gauge.html
[`prometheus::GaugeVec`]: https://docs.rs/prometheus/latest/prometheus/type.GaugeVec.html
[`prometheus::Histogram`]: https://docs.rs/prometheus/latest/prometheus/struct.Histogram.html
[`prometheus::HistogramVec`]: https://docs.rs/prometheus/latest/prometheus/type.HistogramVec.html
[`prometheus::IntCounter`]: https://docs.rs/prometheus/latest/prometheus/type.IntCounter.html
[`prometheus::IntCounterVec`]: https://docs.rs/prometheus/latest/prometheus/type.IntCounterVec.html
[`prometheus::MetricVec`]: https://docs.rs/prometheus/latest/prometheus/core/struct.MetricVec.html
[`prometheus::Registry`]: https://docs.rs/prometheus/latest/prometheus/struct.Registry.html
[`read`-lock]: https://doc.rust-lang.org/stable/std/sync/struct.RwLock.html#method.read
[`Recorder`]: https://docs.rs/metrics-prometheus/latest/metrics_prometheus/struct.Recorder.html
[`tracing`]: https://docs.rs/tracing
[Prometheus]: https://prometheus.io
[Rust]: https://www.rust-lang.org
[VictoriaMetrics]: https://victoriametrics.com
[APACHE]: https://github.com/instrumentisto/metrics-prometheus-rs/blob/main/LICENSE-APACHE
[MIT]: https://github.com/instrumentisto/metrics-prometheus-rs/blob/main/LICENSE-MIT