metrics
+ prometheus
=
❤️
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 vialog
/tracing
ecosystems (and even supportstracing::Span
s for metrics labels).
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 viametrics
facade anytime, without limits. Provides the same overhead of accessing an already registered metric as ametrics::Registry
does:read
-lock on a shardedHashMap
plusArc
cloning. FrozenRecorder
, unable to create new metrics viametrics
facade at all (just no-op in such case). Provides the smallest overhead of accessing an already registered metric: just a regularHashMap
lookup plusArc
cloning.FreezableRecorder
, acting the same way as theRecorder
at first, but being able to.freeze()
and so, becoming aFrozenRecorder
at the end. The overhead of accessing an already registered metric is the same asRecorder
andFrozenRecorder
provide, plusAtomicBool
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.
// By default `prometheus::default_registry()` is used.
let recorder = metrics_prometheus::install();
// Either use `metrics` crate interfaces.
metrics::increment_counter!("count", "whose" => "mine", "kind" => "owned");
metrics::increment_counter!("count", "whose" => "mine", "kind" => "ref");
metrics::increment_counter!("count", "kind" => "owned", "whose" => "dummy");
// 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::increment_counter!("another");
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).
metrics_prometheus::install(); // panics: 'queries.count' is not a valid metric name metrics::increment_counter!("queries.count");
-
The same metric should use always the same set of labels:
metrics_prometheus::install(); metrics::increment_counter!("count"); // panics: Inconsistent label cardinality, expect 0 label values, but got 1 metrics::increment_counter!("count", "whose" => "mine");
metrics_prometheus::install(); metrics::increment_counter!("count", "kind" => "owned"); // panics: label name kind missing in label map metrics::increment_counter!("count", "whose" => "mine");
metrics_prometheus::install(); metrics::increment_counter!("count", "kind" => "owned"); // panics: Inconsistent label cardinality, expect 1 label values, but got 2 metrics::increment_counter!("count", "kind" => "ref", "whose" => "mine");
-
The same name cannot be used for different types of metrics:
metrics_prometheus::install(); metrics::increment_counter!("count"); // panics: Duplicate metrics collector registration attempted metrics::increment_gauge!("count", 1.0);
-
Any metric registered in a
prometheus::Registry
directly, without usingmetrics
or this crate interfaces, is not usable viametrics
facade and will cause aprometheus::Error
.metrics_prometheus::install(); prometheus::default_registry() .register(Box::new(prometheus::Gauge::new("value", "help")?))?; // panics: Duplicate metrics collector registration attempted metrics::increment_gauge!("value", 4.5); # Ok::<_, prometheus::Error>(())
-
metrics::Unit
s are not supported, as Prometheus has no notion of ones. Specifying them viametrics
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
.
use metrics_prometheus::failure::strategy;
metrics_prometheus::Recorder::builder()
.with_failure_strategy(strategy::NoOp)
.build_and_install();
// `prometheus::Error` is ignored inside.
metrics::increment_counter!("invalid.name");
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 Instrumentisto Team, https://github.com/instrumentisto
This software is subject to the terms of the Blue Oak Model License 1.0.0. If a copy of the BlueOak-1.0.0 license was not distributed with this file, You can obtain one at https://blueoakcouncil.org/license/1.0.0.