metered-rs
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 declarative and procedural macros to measure your program without altering your logic.
Metered is built with the following principles in mind:
-
high ergonomics but no magic: measuring code should just be a matter of annotating code. Metered lets you build your own metric registries from bare metrics, or will generate one using procedural macros. It does not use shared globals or statics.
-
constant, very low overhead: good ergonomics should not come with an overhead; the only overhead is the one imposed by actual metric back-ends themselves (e.g, counters, gauges, histograms), and those provided in Metered do not allocate after initialization. Metered will generate metric registries as regular Rust
struct
s, so there is no lookup involved with finding a metric. Metered provides both unsynchronized and thread-safe metric back-ends so that single-threaded or share-nothing architectures don't pay for synchronization. Where possible, thread-safe metric back-ends provided by Metered use lock-free data-structures. -
extensible: metrics are just regular types that implement the
Metric
trait with a specific behavior. Metered's macros let you refer to any Rust type, resulting in user-extensible attributes!
Many metrics are only meaningful if we get precise statistics. When it comes to low-latency, high-range histograms, there's nothing better than Gil Tene's High Dynamic Range Histograms and Metered uses the official Rust port by default for its histograms.
Changelog
- 0.9.0:
- Wrapping int metrics instead of under/overflow
- Provide methods to increment or decrement int metrics by more than 1, useful for batched computations
- Add blanket implementations for
Clear
(contributed by @plankton6) - Add len method to
HdrHistogram
(contributed by @plankton6) - Code quality fixes and dependency updates
- 0.8.0:
- Update Metrics via
OnResultMut
rather than anOnResult
to support metrics that require mutable access to the result - for instance to consume aStream
(contributed by @w4)
- Update Metrics via
- 0.7.0:
- Expose inner metric backend
Throughput
type (fixes issue #30) - Implement
Deref
for all top-level metrics - Expose inner metric backend
Throughput
type - Add
skip_cleared
option toerror_count
attribute (contributed by @w4)- Introduce a new
Clearable
trait that exposes behavior for metrics that implementClear
(in an effort of backwards compatibility). Currently only implemented on counters. - Default behavior can be controlled by the a build-time feature,
error-count-skip-cleared-by-default
- Introduce a new
- Expose inner metric backend
- 0.6.0:
- Extend
error_count
macro to allownested
enum error variants to be reported, providing zero-cost error tracking for nested errors (contributed by @w4)
- Extend
- 0.5.0:
- Make inner metrics public (contributed by @nemosupremo)
- Provide
error_count
macro to generate a tailoredErrorCount
metric counting variants for an error enum (contributed by @w4) - Use
Drop
to automatically trigger metrics that don't rely on the result value (affectsInFlight
,ResponseTime
,Throughput
)
- 0.4.0:
- Add allow(missing_docs) to generated structs (This allows to use metered structs in Rust code with lint level warn(missing_docs) or even deny(missing_docs)) (contributed by @reyk)
- Implement
Clear
for generated registries (contributed by @eliaslevy) - Implement
Histogram
andClear
forRefCell<HdrHistogram>
(contributed by @eliaslevy) - Introduce an
Instant
with microsecond precision (contributed by @eliaslevy)- API breaking change:
Instant.elapsed_millis
is renamed toelapsed_time
, and a new associated constant,ONE_SEC
is introduced to specify one second in the instant units.
- API breaking change:
- Make
AtomicTxPerSec
andTxPerSec
visible by reexporting (contributed by @eliaslevy) - Add
StdInstant
as the default type parameter forT: Instant
inTxPerSec
(contributed by @eliaslevy) - Modify HdrHistogram to work with serde_prometheus (contributed by @w4)
- To be used with serde_prometheus and any HTTP server.
- Bumped dependencies:
indexmap
: 1.1 -> 1.3hdrhistogram
: 6.3 -> 7.1parking_lot
: 0.9 -> 0.10
- 0.3.0:
- Fix to preserve span in
async
measured methods. - Update nightly sample for new syntax and Tokio 0.2-alpha (using std futures, will need Rust >= 1.39, nightly or not)
- Updated dependencies to use
syn
,proc-macro2
andquote
1.0
- Fix to preserve span in
- 0.2.2:
- Async support in
#measured
methods don't rely on async closures anymore, so client code will not require theasync_closure
feature gate. - Updated dependency versions
- Async support in
- 0.2.1:
- Under certain circumstances, Serde would serialize "nulls" for
PhantomData
markers inResponseTime
andThroughput
metrics. They are now explicitely excluded.
- Under certain circumstances, Serde would serialize "nulls" for
- 0.2.0:
- Support for
.await
notation users (no moreawait!()
)
- Support for
- 0.1.3:
- Fix for early returns in
#[measure]
'ed methods - Removed usage of crate
AtomicRefCell
which sometimes panicked . - Support for custom registry visibility.
- Support for
async
+await!()
macro users.
- Fix for early returns in
Using Metered
Metered comes with 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 stdResult
)InFlight
: a gauge tracking how many requests are activeResponseTime
: statistics backed by an HdrHistogram of the duration of an expressionThroughput
: 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 trait metered::metric::Metric
.
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 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).
Right now, Metered does not provide bridges to external metric storage or monitoring systems. Such support is planned in separate modules (contributions welcome!).
Required Rust version
Metered works on Rust
stable, starting 1.31.0.
It does not use any nightly features. There may be a nightly
feature flag at some point to use upcoming Rust features (such as const fn
s), and similar features from crates Metered depends on, but this is low priority (contributions welcome).
Example using procedural macros (recommended)
use metered::{metered, Throughput, HitCount};
#[derive(Default, Debug, serde::Serialize)]
pub struct Biz {
metrics: BizMetrics,
}
#[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.
Let's look at biz
's code a second: it's a blocking method that returns after between 0 and 200ms, using rand::random
. Since random
has a random distribution, we can expect the mean sleep time to be around 100ms. That would mean around 10 calls per second per thread.
In the following test, we spawn 5 threads that each will call biz()
200 times. We thus can expect a hit count of 1000, that it will take around 20 seconds (which means 20 samples, since we collect one sample per second), and around 50 calls per second (10 per thread, with 5 threads).
use std::thread;
use std::sync::Arc;
fn test_biz() {
let biz = Arc::new(Biz::default());
let mut threads = Vec::new();
for _ in 0..5 {
let biz = Arc::clone(&biz);
let t = thread::spawn(move || {
for _ in 0..200 {
biz.biz();
}
});
threads.push(t);
}
for t in threads {
t.join().unwrap();
}
// Print the results!
let serialized = serde_yaml::to_string(&*biz).unwrap();
println!("{}", serialized);
}
We can then use serde to serialize our type as YAML:
metrics:
biz:
hit_count: 1000
throughput:
- samples: 20
min: 35
max: 58
mean: 49.75
stdev: 5.146600819958742
90%ile: 55
95%ile: 55
99%ile: 58
99.9%ile: 58
99.99%ile: 58
- ~
We see we indead have a mean of 49.75 calls per second, which corresponds to our expectations.
The Hdr Histogram backing these statistics is able to give much more than fixed percentiles, but this is a practical view when using text. For a better performance analysis, please watch Gil Tene's talks ;-).
Macro Reference
metered
attribute
The #[metered(registry = YourRegistryName, registry_expr = self.wrapper.my_registry)]
registry
is mandatory and must be a valid Rust ident.
registry_expr
defaults to self.metrics
, alternate values must be a valid Rust expression. This setting lets you configure the expression which resolves to the registry. Please note that this triggers an immutable borrow of that expression.
visibility
defaults to pub(crate)
, and must be a valid struct Rust visibility (e.g, pub
, <nothing>
, pub(self)
, etc). This setting lets you alter the visibility of the generated registry struct
s. The registry fields are always public and named after snake cased methods or metrics.
measure
attribute
The Single metric:
#[measure(path::to::MyMetric<u64>)]
or:
#[measure(type = path::to::MyMetric<u64>)]
Multiple metrics:
#[measure([path::to::MyMetric<u64>, path::AnotherMetric])]
or
#[measure(type = [path::to::MyMetric<u64>, path::AnotherMetric])]
The type
keyword is allowed because other keywords are planned for future extra attributes (e.g, instantation options).
When measure
attribute is applied to an impl
block, it applies for every method that has a measure
attribute. If a method does not need extra measure infos, it is possible to annotate it with simply #[measure]
and the impl
block's measure
configuration will be applied.
The measure
keyword can be added several times on an impl
block or method, which will add to the list of metrics applied. Adding the same metric several time will lead in a name clash.
Design
Metered's custom attribute parsing supports using reserved keywords and arbitrary Rust syntax. The code has been extracted to the Synattra project, which provides useful methods on top of the Syn parser for Attribute parsing.
Metered's metrics can wrap any piece of code, regardless of whether they're async
blocks or not, using hygienic macros to emulate an approach similar to aspect-oriented programming. That code has been extracted to the Aspect-rs project!
License
Licensed under either of
- Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.