Measured. A metrics crate.
This crate was born out of a desire for better ergonomics dealing with prometheus, with the added extra goal of minimizing small allocations to reduce memory fragmentation.
Basic Usage
The most basic usage is defining a single counter. This is very easy.
use Counter;
use CheckedMetricName;
use TextEncoder;
// create a counter
let counter = new;
// increment the counter value
counter.get_metric.inc;
// sample the counter and encode the value to a textual format.
let mut text_encoder = new;
let name = from_static;
counter.collect_into;
let bytes = text_encoder.finish;
With labels
It's common to have labels added to your metrics, such as adding an operation type. When all possible values
can be determined at compile time, you can define the label value as a [FixedCardinalityLabel] enum.
Multiple label pairs are collected into a LabelGroup.
use ;
use CheckedMetricName;
use TextEncoder;
// Define a fixed cardinality label
// Define a label group, consisting of 1 or more label values
// create a counter vec
let counters = new;
// increment the counter at a given label
counters.inc;
counters.inc;
// sample the counters and encode the values to a textual format.
let mut text_encoder = new;
let name = from_static;
counters.collect_into;
let bytes = text_encoder.finish;
With dynamic labels
Sometimes, the labels cannot be determined at compile time, but they can be determine at the start of the program.
This might be the paths of a RESTful API. For efficiency,
measured offers a trait called FixedCardinalityDynamicLabel that allows for compact encoding.
Implementations of FixedCardinalityDynamicLabel are provided for you,
notably [indexmap::IndexSet] and [lasso::RodeoReader].
I recommend the latter for string-based labels that are not &'static as it will offer the most efficient use of memory.
use ;
use CheckedMetricName;
use TextEncoder;
// Define a label group, consisting of 1 or more label values
// initialise your fixed cardinality set
let set = MyLabelGroupSet ;
// create a counter vec
let counters = new;
// increment the counter at a given label
counters.inc;
counters.inc;
// sample the counters and encode the values to a textual format.
let mut text_encoder = new;
let name = from_static;
counters.collect_into;
let bytes = text_encoder.finish;
In the rare case that the label cannot be determined even at startup, you can still use them. You will have to make use of the
DynamicLabel trait. One implementation for string data is provided in the form of [lasso::ThreadedRodeo].
It's not advised to use this for high cardinality labels, but if you must, this still offers good performance.
use ;
use CheckedMetricName;
use TextEncoder;
// Define a label group, consisting of 1 or more label values
// initialise your dynamic cardinality set
let set = MyLabelGroupSet ;
// create a counter vec
let counters = new;
// increment the counter at a given label
counters.inc;
counters.inc;
// sample the counters and encode the values to a textual format.
let mut text_encoder = new;
let name = from_static;
counters.collect_into;
let bytes = text_encoder.finish;
Prometheus vs Memory Fragmentation
The prometheus crate allows you to very quickly
start recording metrics for your application and expose a text-based scrape endpoint. However, the implementation
can quickly lead to memory fragmentation issues.
For example, let's look at IntCounterVec. It's an alias for MetricVec<CounterVecBuilder<AtomicU64>>. MetricVec has the following definition:
pub struct MetricVec<T: MetricVecBuilder> {
pub(crate) v: Arc<MetricVecCore<T>>,
}
pub(crate) struct MetricVecCore<T: MetricVecBuilder> {
pub children: RwLock<HashMap<u64, T::M>>,
// ...
}
And for our int counter, T::M here is
pub struct GenericCounter<P: Atomic> {
v: Arc<Value<P>>,
}
pub struct Value<P: Atomic> {
pub val: P,
pub label_pairs: Vec<LabelPair>,
// ...
}
pub struct LabelPair {
name: ::protobuf::SingularField<::std::string::String>,
value: ::protobuf::SingularField<::std::string::String>,
// ...
}
So, if we have a counter vec with 3 different labels, and a totel of 24 unique label groups, then we will have
- 1 allocation for the
MetricVecArc - 1 allocation for the
MetricVecCoreHashMap - 24 allocations for the counter value
Arc - 24 allocations for the label pairs
Vec - 144 allocations for the
Strings in theLabelPair
Totalling 194 small allocations.
There's nothing wrong with small allocations necessarily, but since these are long-lived allocations that are not allocated inside of an arena, it can lead to fragmentation issues where each small alloc can occupy many different allocator pages and prevent them from being freed.
Compared to this crate, measured only needs 1 allocation for the HashMap.
If you have semi-dynamic string labels (such as REST API path slugs) then that would add 4 allocations for
a RodeoReader or 2 allocations for an IndexSet to track them.
And while it's bad form to have extremely high-cardinality metrics, this crate can easily handle 100,000 unique label groups with just a few large allocations.
Comparisons to the metrics family of crates
The metrics facade crate and
metrics_exporter_prometheus
implementation add a lot of complexity to exposing metrics. They also still alloc an Arc<AtomicU64> per individual counter
which does not solve the problem of memory fragmentation.