[metrics] + [prometheus] = ❤️

Rust docs CI Rust 1.65+ Unsafe Forbidden

API Docs | Changelog

[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::Spans 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 defaultprometheus::defaultregistry()` is used. let recorder = metricsprometheus::install();

// Either use metrics crate interfaces. metrics::incrementcounter!("count", "whose" => "mine", "kind" => "owned"); metrics::incrementcounter!("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() .encodetostring(&prometheus::defaultregistry().gather())?; asserteq!( 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::describecounter!("count", "Example of counter."); metrics::describegauge!("value", "Example of gauge.");

let report = prometheus::TextEncoder::new() .encodetostring(&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::describecounter!("another", "Yet another counter."); metrics::incrementcounter!("another");

let report = prometheus::TextEncoder::new() .encodetostring(&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.

[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;

metricsprometheus::Recorder::builder() .withfailurestrategy(strategy::NoOp) .buildand_install();

// prometheus::Error is ignored inside. metrics::increment_counter!("invalid.name");

let stats = prometheus::defaultregistry().gather(); asserteq!(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.