Expand description

Fast, ergonomic metrics for Rust!

Metered helps you measure the performance of your programs in production. Inspired by Coda Hale’s Java metrics library, Metered makes live measurements easy by providing measurement declarative and procedural macros, and a variety of useful metrics ready out-of-the-box:

  • HitCount: a counter tracking how much a piece of code was hit.
  • ErrorCount: a counter tracking how many errors were returned – (works on any expression returning a std Result)
  • InFlight: a gauge tracking how many requests are active
  • ResponseTime: statistics backed by an HdrHistogram of the duration of an expression
  • Throughput: statistics backed by an HdrHistogram of how many times an expression is called per second.

These metrics are usually applied to methods, using provided procedural macros that generate the boilerplate.

To achieve higher performance, these stock metrics can be customized to use non-thread safe (!Sync/!Send) datastructures, but they default to thread-safe datastructures implemented using lock-free strategies where possible. This is an ergonomical choice to provide defaults that work in all situations.

Metered is designed as a zero-overhead abstraction – in the sense that the higher-level ergonomics should not cost over manually adding metrics. Notably, stock metrics will not allocate memory after they’re initialized the first time. However, they are triggered at every method call and it can be interesting to use lighter metrics (e.g HitCount) in hot code paths and favour heavier metrics (Throughput, ResponseTime) in higher-level entry points.

If a metric you need is missing, or if you want to customize a metric (for instance, to track how many times a specific error occurs, or react depending on your return type), it is possible to implement your own metrics simply by implementing the Metric trait .

Metered does not use statics or shared global state. Instead, it lets you either build your own metric registry using the metrics you need, or can generate a metric registry for you using method attributes. Metered will generate one registry per impl block annotated with the metered attribute, under the name provided as the registry parameter. By default, Metered will expect the registry to be accessed as self.metrics but the expression can be overridden with the registry_expr attribute parameter. See the demos for more examples.

Metered will generate metric registries that derive std::fmt::Debug and serde::Serialize to extract your metrics easily. Metered generates one sub-registry per method annotated with the measure attribute, hence organizing metrics hierarchically. This ensures access time to metrics in generated registries is always constant (and, when possible, cache-friendly), without any overhead other than the metric itself.

Metered will happily measure any method, whether it is async or not, and the metrics will work as expected (e.g, ResponseTime will return the completion time across await’ed invocations).

Metered’s serialized metrics can be used in conjunction with serde_prometheus to publish metrics to Prometheus.


use metered::{metered, Throughput, HitCount};

#[derive(Default, Debug)]
pub struct Biz {
    metrics: BizMetrics,
}

#[metered::metered(registry = BizMetrics)]
impl Biz {
    #[measure([HitCount, Throughput])]
    pub fn biz(&self) {        
        let delay = std::time::Duration::from_millis(rand::random::<u64>() % 200);
        std::thread::sleep(delay);
    }   
}

In the snippet above, we will measure the HitCount and Throughput of the biz method.

This works by first annotating the impl block with the metered annotation and specifying the name Metered should give to the metric registry (here BizMetrics). Later, Metered will assume the expression to access that repository is self.metrics, hence we need a metrics field with the BizMetrics type in Biz. It would be possible to use another field name by specificying another registry expression, such as #[metered(registry = BizMetrics, registry_expr = self.my_custom_metrics)].

Then, we must annotate which methods we wish to measure using the measure attribute, specifying the metrics we wish to apply: the metrics here are simply types of structures implementing the Metric trait, and you can define your own. Since there is no magic, we must ensure self.metrics can be accessed, and this will only work on methods with a &self or &mut self receiver.

Example of manually using metrics

use metered::{measure, HitCount, ErrorCount};

#[derive(Default, Debug)]
struct TestMetrics {
    hit_count: HitCount,
    error_count: ErrorCount,
}

fn test(should_fail: bool, metrics: &TestMetrics) -> Result<u32, &'static str> {
    let hit_count = &metrics.hit_count;
    let error_count = &metrics.error_count;
    measure!(hit_count, {
        measure!(error_count, {
            if should_fail {
                Err("Failed!")
            } else {
                Ok(42)
            }
        })
    })
}

The code above shows how different metrics compose, and in general the kind of boilerplate generated by the #[metered] procedural macro.

Re-exports

pub use metric::Counter;
pub use metric::Gauge;
pub use metric::Histogram;
pub use metric::Metric;

Modules

A module providing new-type Atomic wrapper that implements Debug & Serialize.

A module providing a Clear trait which signals metrics to clear their state if applicable.

A module providing common metrics.

A module providing thread-safe and unsynchronized implementations for Histograms, based on HdrHistogram.

A module providing thread-safe and unsynchronized implementations for Counters on various unsized integers.

A module providing thread-safe and unsynchronized implementations for Gauges on various unsized integers.

A module defining the Metric trait and common metric backends.

A module for Time Sources.

Macros

The measure! macro takes a reference to a metric and an expression.

Structs

A metric counting how many times an expression typed std Result as returned an Err variant.

A metric counting how many times an expression as been hit, before it returns.

A metric providing an in-flight gauge, showing how many calls are currently active for an expression.

A metric measuring the response time of an expression, that is the duration the expression needed to complete.

A metric providing a transaction per second count backed by a histogram.

Traits

Re-export this type so 3rd-party crates don’t need to depend on the aspect-rs crate. The Enter trait is called when entering in an aspect, before the wrapped expression is called.

Trait applied to error enums by #[metered::error_count] to identify generated error count structs.

Functions

Serializer for values within a struct generated by metered::metered_error_variants that adds an error_kind label when being serialized by serde_prometheus.

Serializer for values within a struct generated by metered::metered_error_variants that adds an error_kind label when being serialized by serde_prometheus. If the value has been cleared. This operation is a no-op and the value wont be written to the serializer.

Attribute Macros

A procedural macro that generates a new metric that measures the amount of times each variant of an error has been thrown, to be used as crate-specific replacement for metered::ErrorCount.

A procedural macro that generates a metric registry for an impl block.