[metrics] + [prometheus] = ❤️

crates.io Rust 1.65+ Unsafe Forbidden
CI Rust docs

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-2023 Instrumentisto Team, https://github.com/instrumentisto

Licensed under either of Apache License, Version 2.0 or MIT license 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, shall be dual licensed as above, without any additional terms or conditions.