From 482e95348d30d0208767b806e5cafa5eea659e4f Mon Sep 17 00:00:00 2001 From: Harold Dost Date: Tue, 8 Dec 2020 22:21:18 +0100 Subject: [PATCH 1/2] Work not in progress. --- src/histogram.rs | 81 +++++++++++++++++++++++++++++++++++----- static-metric/src/lib.rs | 6 +++ 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/histogram.rs b/src/histogram.rs index a11e7026..c85fd0bc 100644 --- a/src/histogram.rs +++ b/src/histogram.rs @@ -4,6 +4,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::convert::From; +use std::Num; use std::sync::{ atomic::{AtomicU64 as StdAtomicU64, Ordering}, Arc, Mutex, @@ -22,10 +23,14 @@ use crate::vec::{MetricVec, MetricVecBuilder}; /// tailored to broadly measure the response time (in seconds) of a /// network service. Most likely, however, you will be required to define /// buckets customized to your use case. -pub const DEFAULT_BUCKETS: &[f64; 11] = &[ +pub const DEFAULT_BUCKETS_FLOAT: &[f64; 11] = &[ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, ]; +pub const DEFAULT_BUCKETS_INT: &[u64; 11] = &[ + 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, +]; + /// Used for the label that defines the upper bound of a /// bucket of a histogram ("le" -> "less or equal"). pub const BUCKET_LABEL: &str = "le"; @@ -41,9 +46,34 @@ fn check_bucket_label(label: &str) -> Result<()> { Ok(()) } +fn check_and_adjust_buckets(mut buckets: Vec) -> Result> { + if buckets.is_empty() { + buckets = Vec::from(DEFAULT_BUCKETS_INT as &'static [u64]); + } + + for (i, upper_bound) in buckets.iter().enumerate() { + if i < (buckets.len() - 1) && *upper_bound >= buckets[i + 1] { + return Err(Error::Msg(format!( + "histogram buckets must be in increasing \ + order: {} >= {}", + upper_bound, + buckets[i + 1] + ))); + } + } + + let tail = *buckets.last().unwrap(); + if tail.is_sign_positive() && tail.is_infinite() { + // The +Inf bucket is implicit. Remove it here. + buckets.pop(); + } + + Ok(buckets) +} + fn check_and_adjust_buckets(mut buckets: Vec) -> Result> { if buckets.is_empty() { - buckets = Vec::from(DEFAULT_BUCKETS as &'static [f64]); + buckets = Vec::from(DEFAULT_BUCKETS_FLOAT as &'static [f64]); } for (i, upper_bound) in buckets.iter().enumerate() { @@ -70,7 +100,7 @@ fn check_and_adjust_buckets(mut buckets: Vec) -> Result> { /// mandatory to set Name and Help to a non-empty string. All other fields are /// optional and can safely be left at their zero value. #[derive(Clone, Debug)] -pub struct HistogramOpts { +struct HistogramContainer where T: Num { /// A container holding various options. pub common_opts: Opts, @@ -79,17 +109,35 @@ pub struct HistogramOpts { /// values must be sorted in strictly increasing order. There is no need /// to add a highest bucket with +Inf bound, it will be added /// implicitly. The default value is DefBuckets. - pub buckets: Vec, + pub buckets: Vec, } -impl HistogramOpts { +type HistogramOpts = HistogramContainer; +type IntHistogramOpts = HistogramContainer; + +impl HistogramOptsBase for HistogramOpts { /// Create a [`HistogramOpts`] with the `name` and `help` arguments. - pub fn new, S2: Into>(name: S1, help: S2) -> HistogramOpts { + pub fn new, S2: Into>(name: S1, help: S2) -> HistogramOptsBase { HistogramOpts { common_opts: Opts::new(name, help), - buckets: Vec::from(DEFAULT_BUCKETS as &'static [f64]), + buckets: Vec::from(DEFAULT_FLOAT_BUCKETS as &'static [f64]), } } +} + +impl HistogramOptsBase for IntHistogramOpts { + /// Create a [`HistogramOpts`] with the `name` and `help` arguments. + pub fn new, S2: Into>(name: S1, help: S2) -> HistogramOptsBase { + IntHistogramOpts { + common_opts: Opts::new(name, help), + buckets: Vec::from(DEFAULT_INT_BUCKETS as &'static [u64]), + } + } +} + +pub trait HistogramOptsBase { + /// Create a [`HistogramOpts`] with the `name` and `help` arguments. + pub fn new, S2: Into>(name: S1, help: S2) -> HistogramOptsBase; /// `namespace` sets the namespace. pub fn namespace>(mut self, namespace: S) -> Self { @@ -133,7 +181,7 @@ impl HistogramOpts { } /// `buckets` set the buckets. - pub fn buckets(mut self, buckets: Vec) -> Self { + pub fn buckets(mut self, buckets: Vec) -> Self { self.buckets = buckets; self } @@ -149,7 +197,22 @@ impl From for HistogramOpts { fn from(opts: Opts) -> HistogramOpts { HistogramOpts { common_opts: opts, - buckets: Vec::from(DEFAULT_BUCKETS as &'static [f64]), + buckets: Vec::from(DEFAULT_FLOAT_BUCKETS as &'static [f64]), + } + } +} + +impl Describer for IntHistogramOpts { + fn describe(&self) -> Result { + self.common_opts.describe() + } +} + +impl From for IntHistogramOpts { + fn from(opts: Opts) -> IntHistogramOpts { + IntHistogramOpts { + common_opts: opts, + buckets: Vec::from(DEFAULT_INT_BUCKETS as &'static [u64]), } } } diff --git a/static-metric/src/lib.rs b/static-metric/src/lib.rs index 98776506..f0780c71 100644 --- a/static-metric/src/lib.rs +++ b/static-metric/src/lib.rs @@ -96,6 +96,12 @@ pub fn register_static_histogram_vec(input: TokenStream) -> TokenStream { register_static_vec("histogram", input) } +/// Register a `IntHistogramVec` and create static metrics from it. +#[proc_macro] +pub fn register_static_int_histogram_vec(input: TokenStream) -> TokenStream { + register_static_vec("int_histogram", input) +} + /// Procedural macro handler for `register_static_xxx_vec!`. fn register_static_vec(register_type: &str, input: TokenStream) -> TokenStream { let invoking: RegisterMethodInvoking = syn::parse(input).unwrap(); From b98379512b1bf706931915f5569c47c5a42cae83 Mon Sep 17 00:00:00 2001 From: Harold Dost Date: Tue, 9 Feb 2021 21:46:43 +0100 Subject: [PATCH 2/2] Draft of Change for HistogramInt Relates to #254 --- src/histogram.rs | 980 +++++++++++++++++++++++++++++++++++++---------- src/lib.rs | 2 + 2 files changed, 771 insertions(+), 211 deletions(-) diff --git a/src/histogram.rs b/src/histogram.rs index c85fd0bc..110fef9b 100644 --- a/src/histogram.rs +++ b/src/histogram.rs @@ -4,7 +4,6 @@ use std::cell::RefCell; use std::collections::HashMap; use std::convert::From; -use std::Num; use std::sync::{ atomic::{AtomicU64 as StdAtomicU64, Ordering}, Arc, Mutex, @@ -23,13 +22,16 @@ use crate::vec::{MetricVec, MetricVecBuilder}; /// tailored to broadly measure the response time (in seconds) of a /// network service. Most likely, however, you will be required to define /// buckets customized to your use case. -pub const DEFAULT_BUCKETS_FLOAT: &[f64; 11] = &[ +pub const DEFAULT_FLOAT_BUCKETS: &[f64; 11] = &[ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, ]; +/// Alias for new `DEFAULT_FLOAT_BUCKETS` +/// This will be going away. +pub const DEFAULT_BUCKETS: &[f64; 11] = DEFAULT_FLOAT_BUCKETS; -pub const DEFAULT_BUCKETS_INT: &[u64; 11] = &[ - 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, -]; +/// Integer equivalent to `DEFAULT_FLOAT_BUCKETS` +pub const DEFAULT_INT_BUCKETS: &[u64; 11] = + &[5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]; /// Used for the label that defines the upper bound of a /// bucket of a histogram ("le" -> "less or equal"). @@ -46,9 +48,9 @@ fn check_bucket_label(label: &str) -> Result<()> { Ok(()) } -fn check_and_adjust_buckets(mut buckets: Vec) -> Result> { +fn check_and_adjust_int_buckets(mut buckets: Vec) -> Result> { if buckets.is_empty() { - buckets = Vec::from(DEFAULT_BUCKETS_INT as &'static [u64]); + buckets = Vec::from(DEFAULT_INT_BUCKETS as &'static [u64]); } for (i, upper_bound) in buckets.iter().enumerate() { @@ -63,17 +65,17 @@ fn check_and_adjust_buckets(mut buckets: Vec) -> Result> { } let tail = *buckets.last().unwrap(); - if tail.is_sign_positive() && tail.is_infinite() { - // The +Inf bucket is implicit. Remove it here. + if tail == u64::MAX { + // The max bucket is implicit. Remove it here. buckets.pop(); } Ok(buckets) } -fn check_and_adjust_buckets(mut buckets: Vec) -> Result> { +fn check_and_adjust_float_buckets(mut buckets: Vec) -> Result> { if buckets.is_empty() { - buckets = Vec::from(DEFAULT_BUCKETS_FLOAT as &'static [f64]); + buckets = Vec::from(DEFAULT_FLOAT_BUCKETS as &'static [f64]); } for (i, upper_bound) in buckets.iter().enumerate() { @@ -95,12 +97,18 @@ fn check_and_adjust_buckets(mut buckets: Vec) -> Result> { Ok(buckets) } +pub trait ShardWithValueType { + type ValueType: Atomic; +} +pub trait IntoLocal { + +} /// A struct that bundles the options for creating a [`Histogram`] metric. It is /// mandatory to set Name and Help to a non-empty string. All other fields are /// optional and can safely be left at their zero value. #[derive(Clone, Debug)] -struct HistogramContainer where T: Num { +pub struct HistogramContainer { /// A container holding various options. pub common_opts: Opts, @@ -112,12 +120,16 @@ struct HistogramContainer where T: Num { pub buckets: Vec, } -type HistogramOpts = HistogramContainer; -type IntHistogramOpts = HistogramContainer; +/// f64 based HistogramContainer +pub type HistogramOpts = HistogramContainer; -impl HistogramOptsBase for HistogramOpts { +/// u64 base HistogramContainer +/// Might be useful when trying to squeeze more performance out of some metrics calls +pub type IntHistogramOpts = HistogramContainer; + +impl HistogramOpts { /// Create a [`HistogramOpts`] with the `name` and `help` arguments. - pub fn new, S2: Into>(name: S1, help: S2) -> HistogramOptsBase { + pub fn new, S2: Into>(name: S1, help: S2) -> Self { HistogramOpts { common_opts: Opts::new(name, help), buckets: Vec::from(DEFAULT_FLOAT_BUCKETS as &'static [f64]), @@ -125,9 +137,9 @@ impl HistogramOptsBase for HistogramOpts { } } -impl HistogramOptsBase for IntHistogramOpts { +impl IntHistogramOpts { /// Create a [`HistogramOpts`] with the `name` and `help` arguments. - pub fn new, S2: Into>(name: S1, help: S2) -> HistogramOptsBase { + pub fn new, S2: Into>(name: S1, help: S2) -> Self { IntHistogramOpts { common_opts: Opts::new(name, help), buckets: Vec::from(DEFAULT_INT_BUCKETS as &'static [u64]), @@ -135,10 +147,7 @@ impl HistogramOptsBase for IntHistogramOpts { } } -pub trait HistogramOptsBase { - /// Create a [`HistogramOpts`] with the `name` and `help` arguments. - pub fn new, S2: Into>(name: S1, help: S2) -> HistogramOptsBase; - +impl HistogramContainer { /// `namespace` sets the namespace. pub fn namespace>(mut self, namespace: S) -> Self { self.common_opts.namespace = namespace.into(); @@ -179,7 +188,6 @@ pub trait HistogramOptsBase { pub fn fq_name(&self) -> String { self.common_opts.fq_name() } - /// `buckets` set the buckets. pub fn buckets(mut self, buckets: Vec) -> Self { self.buckets = buckets; @@ -221,11 +229,13 @@ impl From for IntHistogramOpts { /// /// See [`HistogramCore`] for details. #[derive(Debug)] -struct Shard { - sum: AtomicF64, +struct GenericShard { + sum: T, count: AtomicU64, buckets: Vec, } +type Shard = GenericShard; +type IntShard = GenericShard; impl Shard { fn new(num_buckets: usize) -> Self { @@ -242,6 +252,21 @@ impl Shard { } } +impl IntShard { + fn new(num_buckets: usize) -> Self { + let mut buckets = Vec::new(); + for _ in 0..num_buckets { + buckets.push(AtomicU64::new(0)); + } + + IntShard { + sum: AtomicU64::new(0), + count: AtomicU64::new(0), + buckets, + } + } +} + /// Index into an array of [`Shard`]s. /// /// Used in conjunction with [`ShardAndCount`] below. @@ -369,7 +394,7 @@ impl ShardAndCount { /// operations switch hot and cold, wait for all `observe` calls to finish on /// the previously hot now cold shard and then expose the consistent snapshot. #[derive(Debug)] -pub struct HistogramCore { +pub struct GenericHistogramCore { desc: Desc, label_pairs: Vec, @@ -384,9 +409,20 @@ pub struct HistogramCore { shard_and_count: ShardAndCount, /// The two shards where `shard_and_count` determines which one is the hot /// and which one the cold at any given point in time. - shards: [Shard; 2], + shards: [U; 2], + + upper_bounds: Vec, +} - upper_bounds: Vec, +pub type HistogramCore = GenericHistogramCore; +pub type IntHistogramCore = GenericHistogramCore; + +impl GenericHistogramCore { + + + fn sample_count(&self) -> u64 { + self.shard_and_count.get().1 + } } impl HistogramCore { @@ -402,7 +438,7 @@ impl HistogramCore { let label_pairs = make_label_pairs(&desc, label_values)?; - let buckets = check_and_adjust_buckets(opts.buckets.clone())?; + let buckets = check_and_adjust_float_buckets(opts.buckets.clone())?; Ok(HistogramCore { desc, @@ -417,7 +453,82 @@ impl HistogramCore { }) } - /// Record a given observation (f64) in the histogram. + /// Make a snapshot of the current histogram state exposed as a Protobuf + /// struct. + // + // Acquire the collect lock, switch the hot and the cold shard, wait for all + // remaining `observe` calls to finish on the previously hot now cold shard, + // snapshot the data, update the now hot shard and reset the cold shard. + pub fn proto(&self) -> proto::Histogram { + let collect_guard = self.collect_lock.lock().expect("Lock poisoned"); + + // `flip` needs to use AcqRel ordering to ensure the lock operation + // above stays above and the histogram operations (especially the shard + // resets) below stay below. + let (cold_shard_index, overall_count) = self.shard_and_count.flip(Ordering::AcqRel); + + let cold_shard = &self.shards[usize::from(cold_shard_index)]; + let hot_shard = &self.shards[usize::from(cold_shard_index.inverse())]; + + // Wait for all currently active `observe` calls on the now cold shard + // to finish. The above call to `flip` redirects all future `observe` + // calls to the other previously cold, now hot, shard. Thus once the + // cold shard counter equals the value of the global counter when the + // shards were flipped, all in-progress `observe` calls are done. With + // all of them done, the cold shard is now in a consistent state. + // + // `observe` uses `Release` ordering. `compare_and_swap` needs to use + // `Acquire` ordering to ensure that (1) one sees all the previous + // `observe` stores to the counter and (2) to ensure the below shard + // modifications happen after this point, thus the shard is not modified + // by any `observe` operations. + while overall_count + != cold_shard.count.compare_and_swap( + overall_count, + // While at it, reset cold shard count on success. + 0, + Ordering::Acquire, + ) + {} + + // Get cold shard sum and reset to 0. + // + // Use `Acquire` for load and `Release` for store to ensure not to + // interfere with previous or upcoming collect calls. + let cold_shard_sum = cold_shard.sum.swap(0.0, Ordering::AcqRel); + + let mut h = proto::Histogram::default(); + h.set_sample_sum(cold_shard_sum as f64); + h.set_sample_count(overall_count); + + let mut cumulative_count = 0; + let mut buckets = Vec::with_capacity(self.upper_bounds.len()); + for (i, upper_bound) in self.upper_bounds.iter().enumerate() { + // Reset the cold shard and update the hot shard. + // + // Use `Acquire` for load and `Release` for store to ensure not to + // interfere with previous or upcoming collect calls. + let cold_bucket_count = cold_shard.buckets[i].swap(0, Ordering::AcqRel); + hot_shard.buckets[i].inc_by(cold_bucket_count); + + cumulative_count += cold_bucket_count; + let mut b = proto::Bucket::default(); + b.set_cumulative_count(cumulative_count); + b.set_upper_bound(*upper_bound as f64); + buckets.push(b); + } + h.set_bucket(from_vec!(buckets)); + + // Update the hot shard. + hot_shard.count.inc_by(overall_count); + hot_shard.sum.inc_by(cold_shard_sum); + + drop(collect_guard); + + h + } + + /// Record a given observation (T) in the histogram. // // First increase the overall observation counter and thus learn which shard // is the current hot shard. Subsequently on the hot shard update the @@ -449,18 +560,55 @@ impl HistogramCore { shard.count.inc_by_with_ordering(1, Ordering::Release); } + fn sample_sum(&self) -> f64 { + // Make sure to not overlap with any collect calls, as they might flip + // the hot and cold shards. + let _guard = self.collect_lock.lock().expect("Lock poisoned"); + + let (shard_index, _count) = self.shard_and_count.get(); + self.shards[shard_index as usize].sum.get() + } + +} + +impl IntHistogramCore { + pub fn new(opts: &IntHistogramOpts, label_values: &[&str]) -> Result { + let desc = opts.describe()?; + + for name in &desc.variable_labels { + check_bucket_label(name)?; + } + for pair in &desc.const_label_pairs { + check_bucket_label(pair.get_name())?; + } + + let label_pairs = make_label_pairs(&desc, label_values)?; + + let buckets = check_and_adjust_int_buckets(opts.buckets.clone())?; + + Ok(IntHistogramCore { + desc, + label_pairs, + + collect_lock: Mutex::new(()), + + shard_and_count: ShardAndCount::new(), + shards: [IntShard::new(buckets.len()), IntShard::new(buckets.len())], + + upper_bounds: buckets, + }) + } + /// Make a snapshot of the current histogram state exposed as a Protobuf /// struct. // // Acquire the collect lock, switch the hot and the cold shard, wait for all // remaining `observe` calls to finish on the previously hot now cold shard, // snapshot the data, update the now hot shard and reset the cold shard. + // See `HistogramCore` implementation for detailed comments. pub fn proto(&self) -> proto::Histogram { let collect_guard = self.collect_lock.lock().expect("Lock poisoned"); - // `flip` needs to use AcqRel ordering to ensure the lock operation - // above stays above and the histogram operations (especially the shard - // resets) below stay below. let (cold_shard_index, overall_count) = self.shard_and_count.flip(Ordering::AcqRel); let cold_shard = &self.shards[usize::from(cold_shard_index)]; @@ -490,30 +638,23 @@ impl HistogramCore { .is_err() {} - // Get cold shard sum and reset to 0. - // - // Use `Acquire` for load and `Release` for store to ensure not to - // interfere with previous or upcoming collect calls. - let cold_shard_sum = cold_shard.sum.swap(0.0, Ordering::AcqRel); + let cold_shard_sum = cold_shard.sum.swap(0, Ordering::AcqRel); let mut h = proto::Histogram::default(); - h.set_sample_sum(cold_shard_sum); + h.set_sample_sum(cold_shard_sum as f64); h.set_sample_count(overall_count); let mut cumulative_count = 0; let mut buckets = Vec::with_capacity(self.upper_bounds.len()); for (i, upper_bound) in self.upper_bounds.iter().enumerate() { // Reset the cold shard and update the hot shard. - // - // Use `Acquire` for load and `Release` for store to ensure not to - // interfere with previous or upcoming collect calls. let cold_bucket_count = cold_shard.buckets[i].swap(0, Ordering::AcqRel); hot_shard.buckets[i].inc_by(cold_bucket_count); cumulative_count += cold_bucket_count; let mut b = proto::Bucket::default(); b.set_cumulative_count(cumulative_count); - b.set_upper_bound(*upper_bound); + b.set_upper_bound((*upper_bound) as f64); buckets.push(b); } h.set_bucket(from_vec!(buckets)); @@ -527,7 +668,39 @@ impl HistogramCore { h } - fn sample_sum(&self) -> f64 { + /// Record a given observation (T) in the histogram. + // + // First increase the overall observation counter and thus learn which shard + // is the current hot shard. Subsequently on the hot shard update the + // corresponding bucket count, adjust the shard's sum and finally increase + // the shard's count. + pub fn observe(&self, v: u64) { + // The collect code path uses `self.shard_and_count` and + // `self.shards[x].count` to ensure not to collect data from a shard + // while observe calls are still operating on it. + // + // To ensure the above, this `inc` needs to use `Acquire` ordering to + // force anything below this line to stay below it. + let (shard_index, _count) = self.shard_and_count.inc(Ordering::Acquire); + + let shard: &IntShard = &self.shards[usize::from(shard_index)]; + + // Try find the bucket. + let mut iter = self + .upper_bounds + .iter() + .enumerate() + .filter(|&(_, f)| v <= *f); + if let Some((i, _)) = iter.next() { + shard.buckets[i].inc_by(1); + } + + shard.sum.inc_by(v); + // Use `Release` ordering to ensure all operations above stay above. + shard.count.inc_by_with_ordering(1, Ordering::Release); + } + + fn sample_sum(&self) -> u64 { // Make sure to not overlap with any collect calls, as they might flip // the hot and cold shards. let _guard = self.collect_lock.lock().expect("Lock poisoned"); @@ -536,11 +709,7 @@ impl HistogramCore { self.shards[shard_index as usize].sum.get() } - fn sample_count(&self) -> u64 { - self.shard_and_count.get().1 - } } - // We have to wrap libc::timespec in order to implement std::fmt::Debug. #[cfg(all(feature = "nightly", target_os = "linux"))] pub struct Timespec(libc::timespec); @@ -640,17 +809,17 @@ mod coarse { /// Alternatively, it can be manually stopped and discarded in order to not record its value. #[must_use = "Timer should be kept in a variable otherwise it cannot observe duration"] #[derive(Debug)] -pub struct HistogramTimer { +pub struct GenericHistogramTimer { /// A histogram for automatic recording of observations. - histogram: Histogram, + histogram: T, /// Whether the timer has already been observed once. observed: bool, /// Starting instant for the timer. start: Instant, } -impl HistogramTimer { - fn new(histogram: Histogram) -> Self { +impl GenericHistogramTimer { + fn new(histogram: T) -> Self { Self { histogram, observed: false, @@ -659,14 +828,19 @@ impl HistogramTimer { } #[cfg(feature = "nightly")] - fn new_coarse(histogram: Histogram) -> Self { - HistogramTimer { + fn new_coarse(histogram: T) -> Self { + GenericHistogramTimer:: { histogram, observed: false, start: Instant::now_coarse(), } } +} + +pub type HistogramTimer = GenericHistogramTimer; +pub type IntHistogramTimer = GenericHistogramTimer; +impl HistogramTimer { /// Observe and record timer duration (in seconds). /// /// It observes the floating-point number of seconds elapsed since the timer @@ -703,7 +877,44 @@ impl HistogramTimer { } } -impl Drop for HistogramTimer { +impl IntHistogramTimer { + /// Observe and record timer duration (in seconds). + /// + /// It observes the floating-point number of seconds elapsed since the timer + /// started, and it records that value to the attached histogram. + pub fn observe_duration(self) { + self.stop_and_record(); + } + + /// Observe, record and return timer duration (in seconds). + /// + /// It observes and returns a floating-point number for seconds elapsed since + /// the timer started, recording that value to the attached histogram. + pub fn stop_and_record(self) -> u64 { + let mut timer = self; + timer.observe(true) + } + + /// Observe and return timer duration (in seconds). + /// + /// It returns a floating-point number of seconds elapsed since the timer started, + /// without recording to any histogram. + pub fn stop_and_discard(self) -> u64 { + let mut timer = self; + timer.observe(false) + } + + fn observe(&mut self, record: bool) -> u64 { + let v = self.start.elapsed_sec(); + self.observed = true; + if record { + self.histogram.observe(v as u64); + } + v as u64 + } +} + +impl Drop for GenericHistogramTimer { fn drop(&mut self) { if !self.observed { self.observe(true); @@ -729,50 +940,29 @@ impl Drop for HistogramTimer { /// [1]: https://prometheus.io/docs/prometheus/latest/querying/functions/#histogram_quantile /// [2]: https://prometheus.io/docs/practices/histograms/ #[derive(Clone, Debug)] -pub struct Histogram { - core: Arc, +pub struct GenericHistogram { + core: Arc, } -impl Histogram { - /// `with_opts` creates a [`Histogram`] with the `opts` options. - pub fn with_opts(opts: HistogramOpts) -> Result { - Histogram::with_opts_and_label_values(&opts, &[]) - } - - fn with_opts_and_label_values( - opts: &HistogramOpts, - label_values: &[&str], - ) -> Result { - let core = HistogramCore::new(opts, label_values)?; - - Ok(Histogram { - core: Arc::new(core), - }) - } -} +impl GenericHistogram { -impl Histogram { - /// Add a single observation to the [`Histogram`]. - pub fn observe(&self, v: f64) { - self.core.observe(v) - } /// Return a [`HistogramTimer`] to track a duration. - pub fn start_timer(&self) -> HistogramTimer { - HistogramTimer::new(self.clone()) + pub fn start_timer(&self) -> GenericHistogramTimer::> { + GenericHistogramTimer::>::new(self.clone()) } /// Return a [`HistogramTimer`] to track a duration. /// It is faster but less precise. #[cfg(feature = "nightly")] - pub fn start_coarse_timer(&self) -> HistogramTimer { - HistogramTimer::new_coarse(self.clone()) + pub fn start_coarse_timer(&self) -> GenericHistogramTimer::> { + GenericHistogramTimer::>::new_coarse(self.clone()) } /// Observe execution time of a closure, in second. - pub fn observe_closure_duration(&self, f: F) -> T + pub fn observe_closure_duration(&self, f: F) -> V where - F: FnOnce() -> T, + F: FnOnce() -> V, { let instant = Instant::now(); let res = f(); @@ -783,9 +973,9 @@ impl Histogram { /// Observe execution time of a closure, in second. #[cfg(feature = "nightly")] - pub fn observe_closure_duration_coarse(&self, f: F) -> T + pub fn observe_closure_duration_coarse(&self, f: F) -> V where - F: FnOnce() -> T, + F: FnOnce() -> V, { let instant = Instant::now_coarse(); let res = f(); @@ -793,21 +983,101 @@ impl Histogram { self.observe(elapsed); res } +} - /// Return a [`LocalHistogram`] for single thread usage. - pub fn local(&self) -> LocalHistogram { - LocalHistogram::new(self.clone()) - } +pub type Histogram = GenericHistogram; +pub type IntHistogram = GenericHistogram; - /// Return accumulated sum of all samples. - pub fn get_sample_sum(&self) -> f64 { - self.core.sample_sum() +impl Histogram { + /// `with_opts` creates a [`Histogram`] with the `opts` options. + pub fn with_opts(opts: HistogramOpts) -> Result { + Histogram::with_opts_and_label_values(&opts, &[]) } - /// Return count of all samples. - pub fn get_sample_count(&self) -> u64 { - self.core.sample_count() - } + fn with_opts_and_label_values( + opts: &HistogramOpts, + label_values: &[&str], + ) -> Result { + let core = HistogramCore::new(opts, label_values)?; + + Ok(Histogram { + core: Arc::new(core), + }) + } + + /// Add a single observation to the [`Histogram`]. + pub fn observe(&self, v: f64) { + self.core.observe(v) + } + + /// Return a [`LocalHistogram`] for single thread usage. + pub fn local(&self) -> LocalHistogram { + LocalHistogram::new(self.clone()) + } + + /// Return accumulated sum of all samples. + pub fn get_sample_sum(&self) -> f64 { + self.core.sample_sum() + } + + /// Return count of all samples. + pub fn get_sample_count(&self) -> u64 { + self.core.sample_count() + } +} + +impl IntHistogram { + /// `with_opts` creates a [`Histogram`] with the `opts` options. + pub fn with_opts(opts: IntHistogramOpts) -> Result { + IntHistogram::with_opts_and_label_values(&opts, &[]) + } + + fn with_opts_and_label_values( + opts: &IntHistogramOpts, + label_values: &[&str], + ) -> Result { + let core = IntHistogramCore::new(opts, label_values)?; + + Ok(IntHistogram { + core: Arc::new(core), + }) + } + + /// Add a single observation to the [`Histogram`]. + pub fn observe(&self, v: u64) { + self.core.observe(v) + } + + /// Return a [`IntLocalHistogram`] for single thread usage. + pub fn local(&self) -> IntLocalHistogram { + IntLocalHistogram::new(self.clone()) + } + + /// Return accumulated sum of all samples. + pub fn get_sample_sum(&self) -> u64 { + self.core.sample_sum() + } + + /// Return count of all samples. + pub fn get_sample_count(&self) -> u64 { + self.core.sample_count() + } +} + +impl Clone for Histogram { + fn clone(&self) -> Histogram { + let core = self.core.clone(); + let lh = Histogram { core }; + lh + } +} + +impl Clone for IntHistogram { + fn clone(&self) -> IntHistogram { + let core = self.core.clone(); + let lh = IntHistogram { core }; + lh + } } impl Metric for Histogram { @@ -822,6 +1092,18 @@ impl Metric for Histogram { } } +impl Metric for IntHistogram { + fn metric(&self) -> proto::Metric { + let mut m = proto::Metric::default(); + m.set_label(from_vec!(self.core.label_pairs.clone())); + + let h = self.core.proto(); + m.set_histogram(h); + + m + } +} + impl Collector for Histogram { fn desc(&self) -> Vec<&Desc> { vec![&self.core.desc] @@ -838,8 +1120,26 @@ impl Collector for Histogram { } } +impl Collector for IntHistogram { + fn desc(&self) -> Vec<&Desc> { + vec![&self.core.desc] + } + + fn collect(&self) -> Vec { + let mut m = proto::MetricFamily::default(); + m.set_name(self.core.desc.fq_name.clone()); + m.set_help(self.core.desc.help.clone()); + m.set_field_type(proto::MetricType::HISTOGRAM); + m.set_metric(from_vec!(vec![self.metric()])); + + vec![m] + } +} + #[derive(Clone, Debug)] pub struct HistogramVecBuilder {} +#[derive(Clone, Debug)] +pub struct IntHistogramVecBuilder {} impl MetricVecBuilder for HistogramVecBuilder { type M = Histogram; @@ -850,6 +1150,15 @@ impl MetricVecBuilder for HistogramVecBuilder { } } +impl MetricVecBuilder for IntHistogramVecBuilder { + type M = IntHistogram; + type P = IntHistogramOpts; + + fn build(&self, opts: &IntHistogramOpts, vals: &[&str]) -> Result { + IntHistogram::with_opts_and_label_values(opts, vals) + } +} + /// A [`Collector`] that bundles a set of Histograms that all share the /// same [`Desc`], but have different values for their variable labels. This is used /// if you want to count the same thing partitioned by various dimensions @@ -876,6 +1185,31 @@ impl HistogramVec { } } +pub type IntHistogramVec = MetricVec; + +impl IntHistogramVec { + /// Create a new [`HistogramVec`] based on the provided + /// [`HistogramOpts`] and partitioned by the given label names. At least + /// one label name must be provided. + pub fn new(opts: IntHistogramOpts, label_names: &[&str]) -> Result { + let variable_names = label_names.iter().map(|s| (*s).to_owned()).collect(); + let opts = opts.variable_labels(variable_names); + let metric_vec = MetricVec::create( + proto::MetricType::HISTOGRAM, + IntHistogramVecBuilder {}, + opts, + )?; + + Ok(metric_vec as IntHistogramVec) + } + + /// Return a `IntLocalHistogramVec` for single thread usage. + pub fn local(&self) -> IntLocalHistogramVec { + let vec = self.clone(); + IntLocalHistogramVec::new(vec) + } +} + /// Create `count` buckets, each `width` wide, where the lowest /// bucket has an upper bound of `start`. The final +Inf bucket is not counted /// and not included in the returned slice. The returned slice is meant to be @@ -904,6 +1238,34 @@ pub fn linear_buckets(start: f64, width: f64, count: usize) -> Result> Ok(buckets) } +/// Create `count` buckets, each `width` wide, where the lowest +/// bucket has an upper bound of `start`. The final +Inf bucket is not counted +/// and not included in the returned slice. The returned slice is meant to be +/// used for the Buckets field of [`IntHistogramOpts`]. +/// +/// The function returns an error if `count` is zero or `width` is zero or +/// negative. +pub fn linear_int_buckets(start: u64, width: u64, count: usize) -> Result> { + if count < 1 { + return Err(Error::Msg(format!( + "LinearBuckets needs a positive count, count: {}", + count + ))); + } + if width <= 0 { + return Err(Error::Msg(format!( + "LinearBuckets needs a width greater then 0, width: {}", + width + ))); + } + + let buckets: Vec<_> = (0..count) + .map(|step| start + width * (step as u64)) + .collect(); + + Ok(buckets) +} + /// Create `count` buckets, where the lowest bucket has an /// upper bound of `start` and each following bucket's upper bound is `factor` /// times the previous bucket's upper bound. The final +Inf bucket is not counted @@ -944,6 +1306,46 @@ pub fn exponential_buckets(start: f64, factor: f64, count: usize) -> Result Result> { + if count < 1 { + return Err(Error::Msg(format!( + "exponential_buckets needs a positive count, count: {}", + count + ))); + } + if start <= 0 { + return Err(Error::Msg(format!( + "exponential_buckets needs a positive start value, \ + start: {}", + start + ))); + } + if factor <= 1 { + return Err(Error::Msg(format!( + "exponential_buckets needs a factor greater than 1, \ + factor: {}", + factor + ))); + } + + let mut next = start; + let mut buckets = Vec::with_capacity(count); + for _ in 0..count { + buckets.push(next); + next *= factor; + } + + Ok(buckets) +} + /// `duration_to_seconds` converts Duration to seconds. #[inline] pub fn duration_to_seconds(d: Duration) -> f64 { @@ -952,19 +1354,151 @@ pub fn duration_to_seconds(d: Duration) -> f64 { } #[derive(Clone, Debug)] -pub struct LocalHistogramCore { - histogram: Histogram, +pub struct GenericLocalHistogramCore { + histogram: GenericHistogram>>, counts: Vec, count: u64, - sum: f64, + sum: T, +} + +pub type LocalHistogramCore = GenericLocalHistogramCore; +pub type IntLocalHistogramCore = GenericLocalHistogramCore; + +impl GenericLocalHistogramCore { + pub fn flush(&mut self) { + // No cached metric, return. + if self.count == 0 { + return; + } + + { + // The collect code path uses `self.shard_and_count` and + // `self.shards[x].count` to ensure not to collect data from a shard + // while observe calls are still operating on it. + // + // To ensure the above, this `inc` needs to use `Acquire` ordering + // to force anything below this line to stay below it. + let (shard_index, _count) = self + .histogram + .core + .shard_and_count + .inc_by(self.count, Ordering::Acquire); + let shard = &self.histogram.core.shards[shard_index as usize]; + + for (i, v) in self.counts.iter().enumerate() { + if *v > 0 { + shard.buckets[i].inc_by(*v); + } + } + + shard.sum.inc_by(self.sum); + // Use `Release` ordering to ensure all operations above stay above. + shard + .count + .inc_by_with_ordering(self.count, Ordering::Release); + } + + self.clear() + } + + fn sample_count(&self) -> u64 { + self.count + } +} + +impl LocalHistogramCore { + fn new(histogram: Histogram) -> LocalHistogramCore { + let counts = vec![0; histogram.core.upper_bounds.len()]; + + LocalHistogramCore { + histogram, + counts, + count: 0, + sum: 0.0, + } + } + + pub fn observe(&mut self, v: f64) { + // Try find the bucket. + let mut iter = self + .histogram + .core + .upper_bounds + .iter() + .enumerate() + .filter(|&(_, f)| v <= *f); + if let Some((i, _)) = iter.next() { + self.counts[i] += 1; + } + + self.count += 1; + self.sum += v; + } + pub fn clear(&mut self) { + for v in &mut self.counts { + *v = 0 + } + + self.count = 0; + self.sum = 0.0; + } + fn sample_sum(&self) -> f64 { + self.sum + } +} + +impl IntLocalHistogramCore { + fn new(histogram: IntHistogram) -> IntLocalHistogramCore { + let counts = vec![0; histogram.core.upper_bounds.len()]; + + IntLocalHistogramCore { + histogram, + counts, + count: 0, + sum: 0, + } + } + + pub fn observe(&mut self, v: u64) { + // Try find the bucket. + let mut iter = self + .histogram + .core + .upper_bounds + .iter() + .enumerate() + .filter(|&(_, f)| v <= *f); + if let Some((i, _)) = iter.next() { + self.counts[i] += 1; + } + + self.count += 1; + self.sum += v; + } + + pub fn clear(&mut self) { + for v in &mut self.counts { + *v = 0 + } + + self.count = 0; + self.sum = 0; + } + + fn sample_sum(&self) -> u64 { + self.sum + } } /// An unsync [`Histogram`]. #[derive(Debug)] -pub struct LocalHistogram { - core: RefCell, +pub struct GenericLocalHistogram { + core: RefCell, } +pub type LocalHistogram = GenericLocalHistogram; +pub type IntLocalHistogram = GenericLocalHistogram; + impl Clone for LocalHistogram { fn clone(&self) -> LocalHistogram { let core = self.core.clone(); @@ -974,19 +1508,31 @@ impl Clone for LocalHistogram { } } -/// An unsync [`HistogramTimer`]. -#[must_use = "Timer should be kept in a variable otherwise it cannot observe duration"] +impl Clone for IntLocalHistogram { + fn clone(&self) -> IntLocalHistogram { + let core = self.core.clone(); + let lh = IntLocalHistogram { core }; + lh.clear(); + lh + } +} + +/// An unsync [`HistogramTimer`]ec#[must_use = "Timer should be kept in a variable otherwise it cannot observe duration"] #[derive(Debug)] -pub struct LocalHistogramTimer { +pub struct GenericLocalHistogramTimer { /// A local histogram for automatic recording of observations. - local: LocalHistogram, + local: T, /// Whether the timer has already been observed once. observed: bool, /// Starting instant for the timer. start: Instant, } +pub type LocalHistogramTimer = GenericLocalHistogramTimer; +pub type IntLocalHistogramTimer = GenericLocalHistogramTimer; + impl LocalHistogramTimer { + #[cfg(feature = "nightly")] fn new(histogram: LocalHistogram) -> Self { Self { local: histogram, @@ -1004,23 +1550,6 @@ impl LocalHistogramTimer { } } - /// Observe and record timer duration (in seconds). - /// - /// It observes the floating-point number of seconds elapsed since the timer - /// started, and it records that value to the attached histogram. - pub fn observe_duration(self) { - self.stop_and_record(); - } - - /// Observe, record and return timer duration (in seconds). - /// - /// It observes and returns a floating-point number for seconds elapsed since - /// the timer started, recording that value to the attached histogram. - pub fn stop_and_record(self) -> f64 { - let mut timer = self; - timer.observe(true) - } - /// Observe and return timer duration (in seconds). /// /// It returns a floating-point number of seconds elapsed since the timer started, @@ -1040,97 +1569,72 @@ impl LocalHistogramTimer { } } -impl Drop for LocalHistogramTimer { - fn drop(&mut self) { - if !self.observed { - self.observe(true); +impl IntLocalHistogramTimer { + fn new(histogram: IntLocalHistogram) -> Self { + Self { + local: histogram, + observed: false, + start: Instant::now(), } } -} -impl LocalHistogramCore { - fn new(histogram: Histogram) -> LocalHistogramCore { - let counts = vec![0; histogram.core.upper_bounds.len()]; - - LocalHistogramCore { - histogram, - counts, - count: 0, - sum: 0.0, + #[cfg(feature = "nightly")] + fn new_coarse(histogram: IntLocalHistogram) -> Self { + Self { + local: histogram, + observed: false, + start: Instant::now_coarse(), } } - pub fn observe(&mut self, v: f64) { - // Try find the bucket. - let mut iter = self - .histogram - .core - .upper_bounds - .iter() - .enumerate() - .filter(|&(_, f)| v <= *f); - if let Some((i, _)) = iter.next() { - self.counts[i] += 1; - } - - self.count += 1; - self.sum += v; + /// Observe and return timer duration (in seconds). + /// + /// It returns a floating-point number of seconds elapsed since the timer started, + /// without recording to any histogram. + pub fn stop_and_discard(self) -> u64 { + let mut timer = self; + timer.observe(false) } - pub fn clear(&mut self) { - for v in &mut self.counts { - *v = 0 + fn observe(&mut self, record: bool) -> u64 { + let v = self.start.elapsed_sec(); + self.observed = true; + if record { + self.local.observe(v); } - - self.count = 0; - self.sum = 0.0; + v } +} - pub fn flush(&mut self) { - // No cached metric, return. - if self.count == 0 { - return; - } - - { - // The collect code path uses `self.shard_and_count` and - // `self.shards[x].count` to ensure not to collect data from a shard - // while observe calls are still operating on it. - // - // To ensure the above, this `inc` needs to use `Acquire` ordering - // to force anything below this line to stay below it. - let (shard_index, _count) = self - .histogram - .core - .shard_and_count - .inc_by(self.count, Ordering::Acquire); - let shard = &self.histogram.core.shards[shard_index as usize]; - - for (i, v) in self.counts.iter().enumerate() { - if *v > 0 { - shard.buckets[i].inc_by(*v); - } - } +impl GenericLocalHistogramTimer { - shard.sum.inc_by(self.sum); - // Use `Release` ordering to ensure all operations above stay above. - shard - .count - .inc_by_with_ordering(self.count, Ordering::Release); - } - - self.clear() + /// Observe and record timer duration (in seconds). + /// + /// It observes the floating-point number of seconds elapsed since the timer + /// started, and it records that value to the attached histogram. + pub fn observe_duration(self) { + self.stop_and_record(); } - fn sample_sum(&self) -> f64 { - self.sum + /// Observe, record and return timer duration (in seconds). + /// + /// It observes and returns a floating-point number for seconds elapsed since + /// the timer started, recording that value to the attached histogram. + pub fn stop_and_record(self) -> f64 { + let mut timer = self; + timer.observe(true) } +} - fn sample_count(&self) -> u64 { - self.count +impl Drop for GenericLocalHistogramTimer { + fn drop(&mut self) { + if !self.observed { + self.observe(true); + } } } + impl LocalHistogram { fn new(histogram: Histogram) -> LocalHistogram { let core = LocalHistogramCore::new(histogram); @@ -1202,6 +1706,12 @@ impl LocalHistogram { } } +impl Drop for GenericLocalHistogram { + fn drop(&mut self) { + self.flush() + } +} + impl LocalMetric for LocalHistogram { /// Flush the local metrics to the [`Histogram`] metric. fn flush(&self) { @@ -1209,17 +1719,30 @@ impl LocalMetric for LocalHistogram { } } -impl Drop for LocalHistogram { - fn drop(&mut self) { - self.flush() +impl LocalMetric for IntLocalHistogram { + /// Flush the local metrics to the [`Histogram`] metric. + fn flush(&self) { + IntLocalHistogram::flush(self); } } -/// An unsync [`HistogramVec`]. +/// An unsync [`GenericHistogramVec`]. #[derive(Debug)] -pub struct LocalHistogramVec { - vec: HistogramVec, - local: HashMap, +pub struct GenericLocalHistogramVec { + vec: T, + local: HashMap, +} + +pub type LocalHistogramVec = GenericLocalHistogramVec; +pub type IntLocalHistogramVec = GenericLocalHistogramVec; + +impl GenericLocalHistogramVec { + /// Flush the local metrics to the [`HistogramVec`] metric. + pub fn flush(&self) { + for h in self.local.values() { + h.flush(); + } + } } impl LocalHistogramVec { @@ -1228,6 +1751,13 @@ impl LocalHistogramVec { LocalHistogramVec { vec, local } } + /// Remove a [`LocalHistogram`] by label values. + /// See more [`MetricVec::remove_label_values`]. + pub fn remove_label_values(&mut self, vals: &[&str]) -> Result<()> { + let hash = self.vec.v.hash_label_values(vals)?; + self.local.remove(&hash); + self.vec.v.delete_label_values(vals) + } /// Get a [`LocalHistogram`] by label values. /// See more [`MetricVec::with_label_values`]. pub fn with_label_values<'a>(&'a mut self, vals: &[&str]) -> &'a LocalHistogram { @@ -1238,6 +1768,14 @@ impl LocalHistogramVec { .or_insert_with(|| vec.with_label_values(vals).local()) } +} + +impl IntLocalHistogramVec { + fn new(vec: IntHistogramVec) -> IntLocalHistogramVec { + let local = HashMap::with_capacity(vec.v.children.read().len()); + IntLocalHistogramVec { vec, local } + } + /// Remove a [`LocalHistogram`] by label values. /// See more [`MetricVec::remove_label_values`]. pub fn remove_label_values(&mut self, vals: &[&str]) -> Result<()> { @@ -1246,12 +1784,16 @@ impl LocalHistogramVec { self.vec.v.delete_label_values(vals) } - /// Flush the local metrics to the [`HistogramVec`] metric. - pub fn flush(&self) { - for h in self.local.values() { - h.flush(); - } + /// Get a [`IntLocalHistogram`] by label values. + /// See more [`MetricVec::with_label_values`]. + pub fn with_label_values<'a>(&'a mut self, vals: &[&str]) -> &'a IntLocalHistogram { + let hash = self.vec.v.hash_label_values(vals).unwrap(); + let vec = &self.vec; + self.local + .entry(hash) + .or_insert_with(|| vec.with_label_values(vals).local()) } + } impl LocalMetric for LocalHistogramVec { @@ -1267,6 +1809,19 @@ impl Clone for LocalHistogramVec { } } +impl LocalMetric for IntLocalHistogramVec { + /// Flush the local metrics to the [`HistogramVec`] metric. + fn flush(&self) { + IntLocalHistogramVec::flush(self) + } +} + +impl Clone for IntLocalHistogramVec { + fn clone(&self) -> IntLocalHistogramVec { + IntLocalHistogram::new(self.vec.clone()) + } +} + #[cfg(test)] mod tests { use std::f64::{EPSILON, INFINITY}; @@ -1304,7 +1859,10 @@ mod tests { let proto_histogram = m.get_histogram(); assert_eq!(proto_histogram.get_sample_count(), 3); assert!(proto_histogram.get_sample_sum() >= 1.5); - assert_eq!(proto_histogram.get_bucket().len(), DEFAULT_BUCKETS.len()); + assert_eq!( + proto_histogram.get_bucket().len(), + DEFAULT_FLOAT_BUCKETS.len() + ); let buckets = vec![1.0, 2.0, 3.0]; let opts = HistogramOpts::new("test2", "test help").buckets(buckets.clone()); @@ -1370,14 +1928,14 @@ mod tests { #[test] fn test_buckets_invalidation() { let table = vec![ - (vec![], true, DEFAULT_BUCKETS.len()), + (vec![], true, DEFAULT_FLOAT_BUCKETS.len()), (vec![-2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0], true, 7), (vec![-2.0, -1.0, -0.5, 10.0, 0.5, 1.0, 2.0], false, 7), (vec![-2.0, -1.0, -0.5, 0.0, 0.5, 1.0, INFINITY], true, 6), ]; for (buckets, is_ok, length) in table { - let got = check_and_adjust_buckets(buckets); + let got = check_and_adjust_float_buckets(buckets); assert_eq!(got.is_ok(), is_ok); if is_ok { assert_eq!(got.unwrap().len(), length); diff --git a/src/lib.rs b/src/lib.rs index a3bb9f68..870a0652 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -217,9 +217,11 @@ pub use self::encoder::TextEncoder; pub use self::encoder::{PROTOBUF_FORMAT, TEXT_FORMAT}; pub use self::errors::{Error, Result}; pub use self::gauge::{Gauge, GaugeVec, IntGauge, IntGaugeVec}; +#[deprecated(since = "0.12.0", note = "Please use DEFAULT_FLOAT_BUCKETS")] pub use self::histogram::DEFAULT_BUCKETS; pub use self::histogram::{exponential_buckets, linear_buckets}; pub use self::histogram::{Histogram, HistogramOpts, HistogramTimer, HistogramVec}; +pub use self::histogram::{DEFAULT_FLOAT_BUCKETS, DEFAULT_INT_BUCKETS}; pub use self::metrics::Opts; #[cfg(feature = "push")] pub use self::push::{