Expand description

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:

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:

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:

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::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(),
);

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::counter!("queries.count").increment(1);
  • The same metric should use always the same set of labels:

    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);
    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);
    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:

    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.

    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);
  • metrics::Units 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::Errors 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::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::Strategys, 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.

Modules

Structs

Functions