Add metrics-facade crate to provide global macros. (#20)

Add support for a `log`-esque crate that can provide globally-installed metrics collection and ingest via basic macros.
This commit is contained in:
Toby Lawrence 2019-06-11 11:54:27 -04:00 committed by GitHub
parent 2046be737d
commit 1de4343fc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 938 additions and 43 deletions

View File

@ -1,6 +1,7 @@
[workspace]
members = [
"metrics-core",
"metrics-facade",
"metrics",
"metrics-util",
"metrics-exporter-log",

View File

@ -1,6 +1,6 @@
[package]
name = "metrics-core"
version = "0.3.1"
version = "0.4.0"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"

View File

@ -33,6 +33,37 @@
//! Histograms are a convenient way to measure behavior not only at the median, but at the edges of
//! normal operating behavior.
use futures::future::Future;
use std::borrow::Cow;
use std::time::Duration;
/// An optimized metric key.
///
/// As some metrics might be sent at high frequency, it makes no sense to constantly allocate and
/// reallocate owned [`String`]s when a static [`str`] would suffice. As we don't want to limit
/// callers, though, we opt to use a copy-on-write pointer -- [`Cow`] -- to allow callers
/// flexiblity in how and what they pass.
pub type Key = Cow<'static, str>;
/// A value which can be converted into a nanosecond representation.
///
/// This trait allows us to interchangably accept raw integer time values, ones already in
/// nanoseconds, as well as the more conventional [`Duration`] which is a result of getting the
/// difference between two [`Instant`](std::time::Instant)s.
pub trait AsNanoseconds {
fn as_nanos(&self) -> u64;
}
impl AsNanoseconds for u64 {
fn as_nanos(&self) -> u64 {
*self
}
}
impl AsNanoseconds for Duration {
fn as_nanos(&self) -> u64 {
self.as_nanos() as u64
}
}
/// A value that records metrics.
pub trait Recorder {
@ -43,7 +74,7 @@ pub trait Recorder {
/// counters and gauges usually have slightly different modes of operation.
///
/// For the sake of flexibility on the exporter side, both are provided.
fn record_counter<K: AsRef<str>>(&mut self, key: K, value: u64);
fn record_counter<K: Into<Key>>(&mut self, key: K, value: u64);
/// Records a gauge.
///
@ -52,13 +83,15 @@ pub trait Recorder {
/// counters and gauges usually have slightly different modes of operation.
///
/// For the sake of flexibility on the exporter side, both are provided.
fn record_gauge<K: AsRef<str>>(&mut self, key: K, value: i64);
fn record_gauge<K: Into<Key>>(&mut self, key: K, value: i64);
/// Records a histogram.
///
/// Recorders are expected to tally their own histogram views, so this will be called with all
/// of the underlying observed values, and callers will need to process them accordingly.
fn record_histogram<K: AsRef<str>>(&mut self, key: K, values: &[u64]);
///
/// There is no guarantee that this method will not be called multiple times for the same key.
fn record_histogram<K: Into<Key>>(&mut self, key: K, values: &[u64]);
}
/// A value that holds a point-in-time view of collected metrics.

View File

@ -1,6 +1,6 @@
[package]
name = "metrics-exporter-http"
version = "0.1.1"
version = "0.1.2"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
@ -13,6 +13,6 @@ repository = "https://github.com/metrics-rs/metrics-exporter-http"
documentation = "https://docs.rs/metrics-exporter-http"
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.3" }
metrics-core = { path = "../metrics-core", version = "^0.4" }
hyper = "^0.12"
log = "^0.4"

View File

@ -1,6 +1,6 @@
[package]
name = "metrics-exporter-log"
version = "0.2.0"
version = "0.2.1"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
@ -13,7 +13,7 @@ repository = "https://github.com/metrics-rs/metrics-exporter-log"
documentation = "https://docs.rs/metrics-exporter-log"
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.3" }
metrics-core = { path = "../metrics-core", version = "^0.4" }
log = "^0.4"
futures = "^0.1"
tokio-timer = "^0.2"

3
metrics-facade/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock

27
metrics-facade/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "metrics-facade"
version = "0.1.0"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
license = "MIT"
edition = "2018"
repository = "https://github.com/metrics-rs/metrics-facade"
documentation = "https://docs.rs/metrics-facade"
description = """
A lightweight metrics facade for Rust
"""
categories = ["development-tools::debugging"]
readme = "README.md"
keywords = ["metrics", "facade"]
build = "build.rs"
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.4" }
[dev-dependencies]
log = "^0.4"
[package.metadata.docs.rs]
features = ["std"]
[features]
std = []

13
metrics-facade/build.rs Normal file
View File

@ -0,0 +1,13 @@
//! This build script detects target platforms that lack proper support for
//! atomics and sets `cfg` flags accordingly.
use std::env;
fn main() {
// CAS is not available on thumbv6.
let target = env::var("TARGET").unwrap();
if !target.starts_with("thumbv6") {
println!("cargo:rustc-cfg=atomic_cas");
}
println!("cargo:rerun-if-changed=build.rs");
}

View File

@ -0,0 +1,45 @@
#[macro_use]
extern crate metrics_facade;
use metrics_core::Key;
use metrics_facade::Recorder;
#[allow(dead_code)]
static RECORDER: PrintRecorder = PrintRecorder;
#[derive(Default)]
struct PrintRecorder;
impl Recorder for PrintRecorder {
fn record_counter(&self, key: Key, value: u64) {
println!("metrics -> counter(name={}, value={})", key, value);
}
fn record_gauge(&self, key: Key, value: i64) {
println!("metrics -> gauge(name={}, value={})", key, value);
}
fn record_histogram(&self, key: Key, value: u64) {
println!("metrics -> histogram(name={}, value={})", key, value);
}
}
#[cfg(feature = "std")]
fn init_print_logger() {
let recorder = PrintRecorder::default();
metrics_facade::set_boxed_recorder(Box::new(recorder)).unwrap()
}
#[cfg(not(feature = "std"))]
fn init_print_logger() {
metrics_facade::set_recorder(&RECORDER).unwrap()
}
fn main() {
init_print_logger();
counter!("mycounter", 42);
gauge!("mygauge", 123);
timing!("mytiming", 120, 190);
timing!("mytiming", 70);
value!("myvalue", 666);
}

334
metrics-facade/src/lib.rs Normal file
View File

@ -0,0 +1,334 @@
//! A lightweight metrics facade.
//!
//! The `metrics-facade` crate provides a single metrics API that abstracts over the actual metrics
//! implementation. Libraries can use the metrics API provided by this crate, and the consumer of
//! those libraries can choose the metrics implementation that is most suitable for its use case.
//!
//! If no metrics implementation is selected, the facade falls back to a "noop" implementation that
//! ignores all metrics. The overhead in this case is very small - an atomic load and comparison.
//!
//! # Use
//! The basic use of the facade crate is through the four metrics macros: [`counter!`], [`gauge!`],
//! [`timing!`], and [`value!`]. These macros correspond to updating a counter, updating a gauge,
//! updating a histogram based on a start/end, and updating a histogram with a single value.
//!
//! Both [`timing!`] and [`value!`] are effectively identical in so far as that they both translate
//! to recording a single value to an underlying histogram, but [`timing!`] is provided for
//! contextual consistency: if you're recording a measurement of the time passed during an
//! operation, the end result is a single value, but it's more of a "timing" value than just a
//! "value". The [`timing!`] macro also has a branch to accept the start and end values which
//! allows for a potentially clearer invocation.
//!
//! ## In libraries
//! Libraries should link only to the `metrics-facade` crate, and use the provided macros to record
//! whatever metrics will be useful to downstream consumers.
//!
//! ### Examples
//!
//! ```rust
//! #[macro_use]
//! extern crate metrics_facade;
//!
//! # use std::time::Instant;
//! # pub fn run_query(_: &str) -> u64 { 42 }
//! pub fn process(query: &str) -> u64 {
//! let start = Instant::now();
//! let row_count = run_query(query);
//! let end = Instant::now();
//!
//! timing!("process.query_time", start, end);
//! counter!("process.query_row_count", row_count);
//!
//! row_count
//! }
//! # fn main() {}
//! ```
//!
//! ## In executables
//!
//! Executables should choose a metrics implementation and initialize it early in the runtime of
//! the program. Metrics implementations will typically include a function to do this. Any
//! metrics recordered before the implementation is initialized will be ignored.
//!
//! The executable itself may use the `metrics-facade` crate to record metrics well.
//!
//! ### Warning
//!
//! The metrics system may only be initialized once.
//!
//! # Available metrics implementations
//!
//! Currently, the only available metrics implementation is [metrics].
//!
//! # Implementing a Recorder
//!
//! Recorders implement the [`Recorder`] trait. Here's a basic example which writes the
//! metrics in text form via the `log` crate.
//!
//! ```rust
//! #[macro_use]
//! extern crate log;
//! extern crate metrics_facade;
//! extern crate metrics_core;
//!
//! use metrics_facade::Recorder;
//! use metrics_core::Key;
//!
//! struct LogRecorder;
//!
//! impl Recorder for LogRecorder {
//! fn record_counter(&self, key: Key, value: u64) {
//! info!("counter '{}' -> {}", key, value);
//! }
//!
//! fn record_gauge(&self, key: Key, value: i64) {
//! info!("gauge '{}' -> {}", key, value);
//! }
//!
//! fn record_histogram(&self, key: Key, value: u64) {
//! info!("histogram '{}' -> {}", key, value);
//! }
//! }
//! # fn main() {}
//! ```
//!
//! Recorders are installed by calling the [`set_recorder`] function. Recorders should provide a
//! function that wraps the creation and installation of the recorder:
//!
//! ```rust
//! # extern crate metrics_facade;
//! # extern crate metrics_core;
//! # use metrics_facade::Recorder;
//! # use metrics_core::Key;
//! # struct SimpleRecorder;
//! # impl Recorder for SimpleRecorder {
//! # fn record_counter(&self, _key: Key, _value: u64) {}
//! # fn record_gauge(&self, _key: Key, _value: i64) {}
//! # fn record_histogram(&self, _key: Key, _value: u64) {}
//! # }
//! use metrics_facade::SetRecorderError;
//!
//! static RECORDER: SimpleRecorder = SimpleRecorder;
//!
//! pub fn init() -> Result<(), SetRecorderError> {
//! metrics_facade::set_recorder(&RECORDER)
//! }
//! # fn main() {}
//! ```
//!
//! # Use with `std`
//!
//! `set_recorder` requires you to provide a `&'static Recorder`, which can be hard to
//! obtain if your recorder depends on some runtime configuration. The `set_boxed_recorder`
//! function is available with the `std` Cargo feature. It is identical to `set_recorder` except
//! that it takes a `Box<Recorder>` rather than a `&'static Recorder`:
//!
//! ```rust
//! # extern crate metrics_facade;
//! # extern crate metrics_core;
//! # use metrics_facade::Recorder;
//! # use metrics_core::Key;
//! # struct SimpleRecorder;
//! # impl Recorder for SimpleRecorder {
//! # fn record_counter(&self, _key: Key, _value: u64) {}
//! # fn record_gauge(&self, _key: Key, _value: i64) {}
//! # fn record_histogram(&self, _key: Key, _value: u64) {}
//! # }
//! use metrics_facade::SetRecorderError;
//!
//! # #[cfg(feature = "std")]
//! pub fn init() -> Result<(), SetRecorderError> {
//! metrics_facade::set_boxed_recorder(Box::new(SimpleRecorder))
//! }
//! # fn main() {}
//! ```
//!
//! [metrics]: https://docs.rs/metrics
#![deny(missing_docs)]
use metrics_core::{AsNanoseconds, Key};
#[cfg(feature = "std")]
use std::error;
use std::fmt;
use std::sync::atomic::{AtomicUsize, Ordering};
#[macro_use]
mod macros;
static mut RECORDER: &'static Recorder = &NoopRecorder;
static STATE: AtomicUsize = AtomicUsize::new(0);
const UNINITIALIZED: usize = 0;
const INITIALIZING: usize = 1;
const INITIALIZED: usize = 2;
static SET_RECORDER_ERROR: &'static str =
"attempted to set a recorder after the metrics system was already initialized";
/// A value that records metrics behind the facade.
pub trait Recorder {
/// Records a counter.
///
/// From the perspective of an recorder, a counter and gauge are essentially identical, insofar
/// as they are both a single value tied to a key. From the perspective of a collector,
/// counters and gauges usually have slightly different modes of operation.
///
/// For the sake of flexibility on the exporter side, both are provided.
fn record_counter(&self, key: Key, value: u64);
/// Records a gauge.
///
/// From the perspective of a recorder, a counter and gauge are essentially identical, insofar
/// as they are both a single value tied to a key. From the perspective of a collector,
/// counters and gauges usually have slightly different modes of operation.
///
/// For the sake of flexibility on the exporter side, both are provided.
fn record_gauge(&self, key: Key, value: i64);
/// Records a histogram.
///
/// Recorders are expected to tally their own histogram views, so this will be called with all
/// of the underlying observed values, and callers will need to process them accordingly.
///
/// There is no guarantee that this method will not be called multiple times for the same key.
fn record_histogram(&self, key: Key, value: u64);
}
struct NoopRecorder;
impl Recorder for NoopRecorder {
fn record_counter(&self, _key: Key, _value: u64) {}
fn record_gauge(&self, _key: Key, _value: i64) {}
fn record_histogram(&self, _key: Key, _value: u64) {}
}
/// Sets the global recorder to a `&'static Recorder`.
///
/// This function may only be called once in the lifetime of a program. Any metrics recorded
/// before the call to `set_recorder` occurs will be completely ignored.
///
/// This function does not typically need to be called manually. Metrics implementations should
/// provide an initialization method that installs the recorder internally.
///
/// # Errors
///
/// An error is returned if a recorder has already been set.
#[cfg(atomic_cas)]
pub fn set_recorder(recorder: &'static Recorder) -> Result<(), SetRecorderError> {
set_recorder_inner(|| recorder)
}
/// Sets the global recorder to a `Box<Recorder>`.
///
/// This is a simple convenience wrapper over `set_recorder`, which takes a `Box<Recorder>`
/// rather than a `&'static Recorder`. See the document for [`set_recorder`] for more
/// details.
///
/// Requires the `std` feature.
///
/// # Errors
///
/// An error is returned if a recorder has already been set.
#[cfg(all(feature = "std", atomic_cas))]
pub fn set_boxed_recorder(recorder: Box<Recorder>) -> Result<(), SetRecorderError> {
set_recorder_inner(|| unsafe { &*Box::into_raw(recorder) })
}
#[cfg(atomic_cas)]
fn set_recorder_inner<F>(make_recorder: F) -> Result<(), SetRecorderError>
where
F: FnOnce() -> &'static Recorder,
{
unsafe {
match STATE.compare_and_swap(UNINITIALIZED, INITIALIZING, Ordering::SeqCst) {
UNINITIALIZED => {
RECORDER = make_recorder();
STATE.store(INITIALIZED, Ordering::SeqCst);
Ok(())
}
INITIALIZING => {
while STATE.load(Ordering::SeqCst) == INITIALIZING {}
Err(SetRecorderError(()))
}
_ => Err(SetRecorderError(())),
}
}
}
/// A thread-unsafe version of [`set_recorder`].
///
/// This function is available on all platforms, even those that do not have support for atomics
/// that is need by [`set_recorder`].
///
/// In almost all cases, [`set_recorder`] should be preferred.
///
/// # Safety
///
/// This function is only safe to call when no other metrics initialization function is called
/// while this function still executes.
///
/// This can be upheld by (for example) making sure that **there are no other threads**, and (on
/// embedded) that **interrupts are disabled**.
///
/// It is safe to use other metrics functions while this function runs (including all metrics
/// macros).
pub unsafe fn set_recorder_racy(recorder: &'static Recorder) -> Result<(), SetRecorderError> {
match STATE.load(Ordering::SeqCst) {
UNINITIALIZED => {
RECORDER = recorder;
STATE.store(INITIALIZED, Ordering::SeqCst);
Ok(())
}
INITIALIZING => {
// This is just plain UB, since we were racing another initialization function
unreachable!("set_recorder_racy must not be used with other initialization functions")
}
_ => Err(SetRecorderError(())),
}
}
/// The type returned by [`set_recorder`] if [`set_recorder`] has already been called.
#[derive(Debug)]
pub struct SetRecorderError(());
impl fmt::Display for SetRecorderError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(SET_RECORDER_ERROR)
}
}
// The Error trait is not available in libcore
#[cfg(feature = "std")]
impl error::Error for SetRecorderError {
fn description(&self) -> &str {
SET_RECORDER_ERROR
}
}
/// Returns a reference to the recorder.
///
/// If a recorder has not been set, a no-op implementation is returned.
pub fn recorder() -> &'static Recorder {
unsafe {
if STATE.load(Ordering::SeqCst) != INITIALIZED {
static NOOP: NoopRecorder = NoopRecorder;
&NOOP
} else {
RECORDER
}
}
}
#[doc(hidden)]
pub fn __private_api_record_count<K: Into<Key>>(key: K, value: u64) {
recorder().record_counter(key.into(), value);
}
#[doc(hidden)]
pub fn __private_api_record_gauge<K: Into<Key>>(key: K, value: i64) {
recorder().record_gauge(key.into(), value);
}
#[doc(hidden)]
pub fn __private_api_record_histogram<K: Into<Key>, V: AsNanoseconds>(key: K, value: V) {
recorder().record_histogram(key.into(), value.as_nanos());
}

View File

@ -0,0 +1,112 @@
/// Records a counter.
///
/// Functionally equivalent to calling [`Recorder::record_counter`].
///
/// ### Examples
///
/// ```rust
/// # #[macro_use]
/// # extern crate metrics_facade;
/// fn do_thing() {
/// let count: u64 = 42;
/// counter!("do_thing", count);
/// }
/// # fn main() {}
/// ```
#[macro_export]
macro_rules! counter {
($name:tt, $value:expr) => {{
$crate::__private_api_record_count($name, $value);
}};
}
/// Records a gauge.
///
/// Functionally equivalent to calling [`Recorder::record_gauge`].
///
/// ### Examples
///
/// ```rust
/// # #[macro_use]
/// # extern crate metrics_facade;
/// fn update_current_value() {
/// let value: i64 = -131;
/// gauge!("current_value", value);
/// }
/// # fn main() {}
/// ```
#[macro_export]
macro_rules! gauge {
($name:tt, $value:expr) => {{
$crate::__private_api_record_gauge($name, $value);
}};
}
/// Records a timing.
///
/// Functionally equivalent to calling [`Recorder::record_histogram`].
///
/// ### Examples
///
/// ```rust
/// # #[macro_use]
/// # extern crate metrics_facade;
/// # use std::time::Instant;
/// # fn process() {}
/// fn handle_request() {
/// let start = Instant::now();
/// process();
/// let end = Instant::now();
///
/// // We can pass instances of `Instant` directly:
/// timing!("performance.request_processed", start, end);
///
/// // Or we can pass just the delta:
/// let delta = end - start;
/// timing!("performance.request_processed", delta);
///
/// // And we can even pass unsigned values, both for the start/end notation:
/// let start: u64 = 100;
/// let end: u64 = 200;
/// timing!("performance.request_processed", start, end);
///
/// // And the delta notation:
/// let delta: u64 = end - start;
/// timing!("performance.request_processed", delta);
/// }
/// # fn main() {}
/// ```
#[macro_export]
macro_rules! timing {
($name:tt, $value:expr) => {{
$crate::__private_api_record_histogram($name, $value);
}};
($name:tt, $start:expr, $end:expr) => {{
let delta = $end - $start;
$crate::__private_api_record_histogram($name, delta);
}};
}
/// Records a value.
///
/// Functionally equivalent to calling [`Recorder::record_histogram`].
///
/// ### Examples
///
/// ```rust
/// # #[macro_use]
/// # extern crate metrics_facade;
/// # use std::time::Instant;
/// # fn process() -> u64 { 42 }
/// fn handle_request() {
/// let rows_read = process();
/// value!("client.process_num_rows", rows_read);
/// }
/// # fn main() {}
/// ```
#[macro_export]
macro_rules! value {
($name:tt, $value:expr) => {{
$crate::__private_api_record_histogram($name, $value);
}};
}

View File

@ -1,6 +1,6 @@
[package]
name = "metrics-recorder-prometheus"
version = "0.2.1"
version = "0.2.2"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
@ -13,6 +13,6 @@ repository = "https://github.com/metrics-rs/metrics-recorder-prometheus"
documentation = "https://docs.rs/metrics-recorder-prometheus"
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.3" }
metrics-core = { path = "../metrics-core", version = "^0.4" }
metrics-util = { path = "../metrics-util", version = "^0.2" }
hdrhistogram = "^6.1"

View File

@ -1,6 +1,6 @@
//! Records metrics in the Prometheus exposition format.
use hdrhistogram::Histogram;
use metrics_core::Recorder;
use metrics_core::{Key, Recorder};
use metrics_util::{parse_quantiles, Quantile};
use std::time::SystemTime;
@ -35,8 +35,8 @@ impl PrometheusRecorder {
}
impl Recorder for PrometheusRecorder {
fn record_counter<K: AsRef<str>>(&mut self, key: K, value: u64) {
let label = key.as_ref().replace('.', "_");
fn record_counter<K: Into<Key>>(&mut self, key: K, value: u64) {
let label = key.into().as_ref().replace('.', "_");
self.output.push_str("\n# TYPE ");
self.output.push_str(label.as_str());
self.output.push_str(" counter\n");
@ -46,8 +46,8 @@ impl Recorder for PrometheusRecorder {
self.output.push_str("\n");
}
fn record_gauge<K: AsRef<str>>(&mut self, key: K, value: i64) {
let label = key.as_ref().replace('.', "_");
fn record_gauge<K: Into<Key>>(&mut self, key: K, value: i64) {
let label = key.into().as_ref().replace('.', "_");
self.output.push_str("\n# TYPE ");
self.output.push_str(label.as_str());
self.output.push_str(" gauge\n");
@ -57,7 +57,7 @@ impl Recorder for PrometheusRecorder {
self.output.push_str("\n");
}
fn record_histogram<K: AsRef<str>>(&mut self, key: K, values: &[u64]) {
fn record_histogram<K: Into<Key>>(&mut self, key: K, values: &[u64]) {
let mut sum = 0;
let mut h = Histogram::<u64>::new(3).expect("failed to create histogram");
for value in values {
@ -65,7 +65,7 @@ impl Recorder for PrometheusRecorder {
sum += *value;
}
let label = key.as_ref().replace('.', "_");
let label = key.into().as_ref().replace('.', "_");
self.output.push_str("\n# TYPE ");
self.output.push_str(label.as_str());
self.output.push_str(" summary\n");

View File

@ -1,6 +1,6 @@
[package]
name = "metrics-recorder-text"
version = "0.2.1"
version = "0.2.2"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
@ -13,6 +13,6 @@ repository = "https://github.com/metrics-rs/metrics-recorder-text"
documentation = "https://docs.rs/metrics-recorder-text"
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.3" }
metrics-core = { path = "../metrics-core", version = "^0.4" }
metrics-util = { path = "../metrics-util", version = "^0.2" }
hdrhistogram = "^6.1"

View File

@ -43,7 +43,7 @@
//! ```
//!
use hdrhistogram::Histogram;
use metrics_core::Recorder;
use metrics_core::{Key, Recorder};
use metrics_util::{parse_quantiles, Quantile};
use std::collections::{HashMap, VecDeque};
use std::fmt::Display;
@ -80,25 +80,25 @@ impl TextRecorder {
}
impl Recorder for TextRecorder {
fn record_counter<K: AsRef<str>>(&mut self, key: K, value: u64) {
let (name_parts, name) = name_to_parts(key.as_ref());
fn record_counter<K: Into<Key>>(&mut self, key: K, value: u64) {
let (name_parts, name) = name_to_parts(key.into().as_ref());
let mut values = single_value_to_values(name, value);
self.structure.insert(name_parts, &mut values);
}
fn record_gauge<K: AsRef<str>>(&mut self, key: K, value: i64) {
let (name_parts, name) = name_to_parts(key.as_ref());
fn record_gauge<K: Into<Key>>(&mut self, key: K, value: i64) {
let (name_parts, name) = name_to_parts(key.into().as_ref());
let mut values = single_value_to_values(name, value);
self.structure.insert(name_parts, &mut values);
}
fn record_histogram<K: AsRef<str>>(&mut self, key: K, values: &[u64]) {
fn record_histogram<K: Into<Key>>(&mut self, key: K, values: &[u64]) {
let mut h = Histogram::new(3).expect("failed to create histogram");
for value in values {
h.record(*value).expect("failed to record histogram value");
}
let (name_parts, name) = name_to_parts(key.as_ref());
let (name_parts, name) = name_to_parts(key.into().as_ref());
let mut values = hist_to_values(name, h, &self.quantiles);
self.structure.insert(name_parts, &mut values);
}

View File

@ -31,13 +31,14 @@ name = "histogram"
harness = false
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.3" }
metrics-core = { path = "../metrics-core", version = "^0.4" }
metrics-util = { path = "../metrics-util", version = "^0.2" }
metrics-facade = { path = "../metrics-facade", version = "^0.1", features = ["std"] }
im = "^12"
fxhash = "^0.2"
arc-swap = "^0.3"
parking_lot = "^0.7"
hashbrown = "^0.3"
parking_lot = "^0.8"
hashbrown = "^0.4"
quanta = "^0.3"
futures = "^0.1"
crossbeam-utils = "^0.6"

View File

@ -9,7 +9,7 @@ extern crate metrics_core;
use getopts::Options;
use hdrhistogram::Histogram;
use metrics::{Receiver, Sink};
use metrics_core::{Recorder, Snapshot, SnapshotProvider};
use metrics_core::{Key, Recorder, Snapshot, SnapshotProvider};
use quanta::Clock;
use std::{
env,
@ -292,15 +292,15 @@ impl TotalRecorder {
}
impl Recorder for TotalRecorder {
fn record_counter<K: AsRef<str>>(&mut self, _key: K, value: u64) {
fn record_counter<K: Into<Key>>(&mut self, _key: K, value: u64) {
self.total += value;
}
fn record_gauge<K: AsRef<str>>(&mut self, _key: K, value: i64) {
fn record_gauge<K: Into<Key>>(&mut self, _key: K, value: i64) {
self.total += value as u64;
}
fn record_histogram<K: AsRef<str>>(&mut self, _key: K, values: &[u64]) {
fn record_histogram<K: Into<Key>>(&mut self, _key: K, values: &[u64]) {
self.total += values.len() as u64;
}
}

271
metrics/examples/facade.rs Normal file
View File

@ -0,0 +1,271 @@
#[macro_use]
extern crate log;
extern crate env_logger;
extern crate getopts;
extern crate hdrhistogram;
extern crate metrics;
extern crate metrics_core;
#[macro_use]
extern crate metrics_facade;
use getopts::Options;
use hdrhistogram::Histogram;
use metrics::{Receiver, Sink};
use metrics_core::{Key, Recorder, Snapshot, SnapshotProvider};
use quanta::Clock;
use std::{
env,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
thread,
time::{Duration, Instant},
};
const LOOP_SAMPLE: u64 = 1000;
struct Generator {
t0: Option<u64>,
gauge: i64,
hist: Histogram<u64>,
done: Arc<AtomicBool>,
rate_counter: Arc<AtomicU64>,
clock: Clock,
}
impl Generator {
fn new(done: Arc<AtomicBool>, rate_counter: Arc<AtomicU64>, clock: Clock) -> Generator {
Generator {
t0: None,
gauge: 0,
hist: Histogram::<u64>::new_with_bounds(1, u64::max_value(), 3).unwrap(),
done,
rate_counter,
clock,
}
}
fn run(&mut self) {
let mut counter = 0;
loop {
counter += 1;
if self.done.load(Ordering::Relaxed) {
break;
}
self.gauge += 1;
let t1 = self.clock.now();
if let Some(t0) = self.t0 {
let start = if counter % LOOP_SAMPLE == 0 {
self.clock.now()
} else {
0
};
counter!("ok", 1);
timing!("ok", t1 - t0);
gauge!("total", self.gauge);
if start != 0 {
let delta = self.clock.now() - start;
self.hist.saturating_record(delta);
// We also increment our global counter for the sample rate here.
self.rate_counter
.fetch_add(LOOP_SAMPLE * 3, Ordering::AcqRel);
}
}
self.t0 = Some(t1);
}
}
}
impl Drop for Generator {
fn drop(&mut self) {
info!(
" sender latency: min: {:9} p50: {:9} p95: {:9} p99: {:9} p999: {:9} max: {:9}",
nanos_to_readable(self.hist.min()),
nanos_to_readable(self.hist.value_at_percentile(50.0)),
nanos_to_readable(self.hist.value_at_percentile(95.0)),
nanos_to_readable(self.hist.value_at_percentile(99.0)),
nanos_to_readable(self.hist.value_at_percentile(99.9)),
nanos_to_readable(self.hist.max())
);
}
}
fn print_usage(program: &str, opts: &Options) {
let brief = format!("Usage: {} [options]", program);
print!("{}", opts.usage(&brief));
}
pub fn opts() -> Options {
let mut opts = Options::new();
opts.optopt(
"d",
"duration",
"number of seconds to run the benchmark",
"INTEGER",
);
opts.optopt("p", "producers", "number of producers", "INTEGER");
opts.optflag("h", "help", "print this help menu");
opts
}
fn main() {
env_logger::init();
let args: Vec<String> = env::args().collect();
let program = &args[0];
let opts = opts();
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => {
error!("Failed to parse command line args: {}", f);
return;
}
};
if matches.opt_present("help") {
print_usage(program, &opts);
return;
}
info!("metrics benchmark");
// Build our sink and configure the facets.
let seconds = matches
.opt_str("duration")
.unwrap_or_else(|| "60".to_owned())
.parse()
.unwrap();
let producers = matches
.opt_str("producers")
.unwrap_or_else(|| "1".to_owned())
.parse()
.unwrap();
info!("duration: {}s", seconds);
info!("producers: {}", producers);
let receiver = Receiver::builder()
.histogram(Duration::from_secs(5), Duration::from_millis(100))
.build()
.expect("failed to build receiver");
let controller = receiver.get_controller();
receiver.install();
info!("receiver configured");
// Spin up our sample producers.
let done = Arc::new(AtomicBool::new(false));
let rate_counter = Arc::new(AtomicU64::new(0));
let mut handles = Vec::new();
let clock = Clock::new();
for _ in 0..producers {
let d = done.clone();
let r = rate_counter.clone();
let c = clock.clone();
let handle = thread::spawn(move || {
Generator::new(d, r, c).run();
});
handles.push(handle);
}
// Poll the controller to figure out the sample rate.
let mut total = 0;
let mut t0 = Instant::now();
let mut snapshot_hist = Histogram::<u64>::new_with_bounds(1, u64::max_value(), 3).unwrap();
for _ in 0..seconds {
let t1 = Instant::now();
let start = Instant::now();
let snapshot = controller.get_snapshot().unwrap();
let end = Instant::now();
snapshot_hist.saturating_record(duration_as_nanos(end - start) as u64);
let turn_total = rate_counter.load(Ordering::Acquire);
let turn_delta = turn_total - total;
total = turn_total;
let rate = turn_delta as f64 / (duration_as_nanos(t1 - t0) / 1_000_000_000.0);
info!("sample ingest rate: {:.0} samples/sec", rate);
t0 = t1;
thread::sleep(Duration::new(1, 0));
}
info!("--------------------------------------------------------------------------------");
info!(" ingested samples total: {}", total);
info!(
"snapshot retrieval: min: {:9} p50: {:9} p95: {:9} p99: {:9} p999: {:9} max: {:9}",
nanos_to_readable(snapshot_hist.min()),
nanos_to_readable(snapshot_hist.value_at_percentile(50.0)),
nanos_to_readable(snapshot_hist.value_at_percentile(95.0)),
nanos_to_readable(snapshot_hist.value_at_percentile(99.0)),
nanos_to_readable(snapshot_hist.value_at_percentile(99.9)),
nanos_to_readable(snapshot_hist.max())
);
// Wait for the producers to finish so we can get their stats too.
done.store(true, Ordering::SeqCst);
for handle in handles {
let _ = handle.join();
}
}
struct TotalRecorder {
total: u64,
}
impl TotalRecorder {
pub fn new() -> Self {
Self { total: 0 }
}
pub fn total(&self) -> u64 {
self.total
}
}
impl Recorder for TotalRecorder {
fn record_counter<K: Into<Key>>(&mut self, _key: K, value: u64) {
self.total += value;
}
fn record_gauge<K: Into<Key>>(&mut self, _key: K, value: i64) {
self.total += value as u64;
}
fn record_histogram<K: Into<Key>>(&mut self, _key: K, values: &[u64]) {
self.total += values.len() as u64;
}
}
fn duration_as_nanos(d: Duration) -> f64 {
(d.as_secs() as f64 * 1e9) + d.subsec_nanos() as f64
}
fn nanos_to_readable(t: u64) -> String {
let f = t as f64;
if f < 1_000.0 {
format!("{}ns", f)
} else if f < 1_000_000.0 {
format!("{:.0}μs", f / 1_000.0)
} else if f < 2_000_000_000.0 {
format!("{:.2}ms", f / 1_000_000.0)
} else {
format!("{:.3}s", f / 1_000_000_000.0)
}
}

View File

@ -1,5 +1,6 @@
use crate::common::ValueSnapshot;
use metrics_core::{Recorder, Snapshot as MetricsSnapshot};
use metrics_core::{Key, Recorder, Snapshot as MetricsSnapshot};
use std::borrow::Cow;
/// A point-in-time view of metric data.
#[derive(Default, Debug)]
@ -17,11 +18,14 @@ impl MetricsSnapshot for Snapshot {
/// Records the snapshot to the given recorder.
fn record<R: Recorder>(&self, recorder: &mut R) {
for (key, snapshot) in &self.measurements {
// TODO: switch this to Key::Owned once type_alias_enum_variants lands
// in 1.37.0 (#61682)
let owned_key: Key = Cow::Owned(key.clone());
match snapshot {
ValueSnapshot::Counter(value) => recorder.record_counter(key, *value),
ValueSnapshot::Gauge(value) => recorder.record_gauge(key, *value),
ValueSnapshot::Counter(value) => recorder.record_counter(owned_key.clone(), *value),
ValueSnapshot::Gauge(value) => recorder.record_gauge(owned_key.clone(), *value),
ValueSnapshot::Histogram(stream) => stream.decompress_with(|values| {
recorder.record_histogram(key, values);
recorder.record_histogram(owned_key.clone(), values);
}),
}
}
@ -31,6 +35,7 @@ impl MetricsSnapshot for Snapshot {
#[cfg(test)]
mod tests {
use super::{MetricsSnapshot, Recorder, Snapshot, ValueSnapshot};
use metrics_core::Key;
use metrics_util::StreamingIntegers;
use std::collections::HashMap;
@ -56,18 +61,18 @@ mod tests {
}
impl Recorder for MockRecorder {
fn record_counter<K: AsRef<str>>(&mut self, key: K, value: u64) {
let _ = self.counter.insert(key.as_ref().to_owned(), value);
fn record_counter<K: Into<Key>>(&mut self, key: K, value: u64) {
let _ = self.counter.insert(key.into().to_string(), value);
}
fn record_gauge<K: AsRef<str>>(&mut self, key: K, value: i64) {
let _ = self.gauge.insert(key.as_ref().to_owned(), value);
fn record_gauge<K: Into<Key>>(&mut self, key: K, value: i64) {
let _ = self.gauge.insert(key.into().to_string(), value);
}
fn record_histogram<K: AsRef<str>>(&mut self, key: K, values: &[u64]) {
fn record_histogram<K: Into<Key>>(&mut self, key: K, values: &[u64]) {
let _ = self
.histogram
.insert(key.as_ref().to_owned(), values.to_vec());
.insert(key.into().to_string(), values.to_vec());
}
}

View File

@ -6,9 +6,16 @@ use crate::{
registry::{MetricRegistry, ScopeRegistry},
sink::Sink,
};
use metrics_core::Key;
use metrics_facade::Recorder;
use quanta::{Builder as UpkeepBuilder, Clock, Handle as UpkeepHandle};
use std::cell::RefCell;
use std::sync::Arc;
thread_local! {
static SINK: RefCell<Option<Sink>> = RefCell::new(None);
}
/// Central store for metrics.
///
/// `Receiver` is the nucleus for all metrics operations. While no operations are performed by it
@ -52,6 +59,11 @@ impl Receiver {
Builder::default()
}
/// Installs this receiver as the global metrics facade.
pub fn install(self) {
metrics_facade::set_boxed_recorder(Box::new(self)).unwrap();
}
/// Creates a [`Sink`] bound to this receiver.
pub fn get_sink(&self) -> Sink {
Sink::new(
@ -67,3 +79,41 @@ impl Receiver {
Controller::new(self.metric_registry.clone(), self.scope_registry.clone())
}
}
impl Recorder for Receiver {
fn record_counter(&self, key: Key, value: u64) {
SINK.with(move |sink| {
let mut sink = sink.borrow_mut();
if sink.is_none() {
let new_sink = self.get_sink();
*sink = Some(new_sink);
}
sink.as_mut().unwrap().record_count(key, value);
});
}
fn record_gauge(&self, key: Key, value: i64) {
SINK.with(move |sink| {
let mut sink = sink.borrow_mut();
if sink.is_none() {
let new_sink = self.get_sink();
*sink = Some(new_sink);
}
sink.as_mut().unwrap().record_gauge(key, value);
});
}
fn record_histogram(&self, key: Key, value: u64) {
SINK.with(move |sink| {
let mut sink = sink.borrow_mut();
if sink.is_none() {
let new_sink = self.get_sink();
*sink = Some(new_sink);
}
sink.as_mut().unwrap().record_value(key, value);
});
}
}