wip commit

This commit is contained in:
Toby Lawrence 2020-06-10 21:27:23 -04:00
parent 71ca17754f
commit 35316e5df1
31 changed files with 1156 additions and 346 deletions

View File

@ -5,4 +5,5 @@ members = [
"metrics-util",
"metrics-benchmark",
"metrics-exporter-tcp",
"metrics-exporter-prometheus",
]

View File

@ -4,13 +4,11 @@ version = "0.1.0"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "^0.4"
env_logger = "^0.7"
getopts = "^0.2"
hdrhistogram = "^7.0"
quanta = "^0.5"
metrics = { version = "^0.12", path = "../metrics" }
metrics-util = { version = "^0.3", path = "../metrics-util" }
metrics = { version = "^0.13", path = "../metrics" }
metrics-util = { version = "^0.4", path = "../metrics-util" }

View File

@ -57,7 +57,7 @@ impl Generator {
increment!("ok");
gauge!("total", self.gauge as f64);
histogram!("ok", t1.sub(t0).as_secs_f64());
histogram!("ok", t1.sub(t0));
if let Some(val) = start {
let delta = Instant::now() - val;

View File

@ -0,0 +1,29 @@
[package]
name = "metrics-exporter-prometheus"
version = "0.2.0"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
license = "MIT"
description = "A metrics-compatible exporter that serves a Prometheus scrape endpoint."
homepage = "https://github.com/metrics-rs/metrics"
repository = "https://github.com/metrics-rs/metrics"
documentation = "https://docs.rs/metrics-exporter-prometheus"
readme = "README.md"
categories = ["development-tools::debugging"]
keywords = ["metrics", "telemetry", "prometheus"]
[dependencies]
metrics = { version = "^0.13", path = "../metrics" }
metrics-util = { version = "^0.4", path = "../metrics-util"}
hdrhistogram = "^7.1"
hyper = { version = "^0.13", default-features = false, features = ["tcp"] }
tokio = { version = "^0.2", features = ["rt-core", "tcp", "time", "macros"] }
parking_lot = "^0.10"
[dev-dependencies]
quanta = "^0.5"
tracing = "^0.1"
tracing-subscriber = "^0.2"

View File

@ -0,0 +1,32 @@
use std::thread;
use std::time::Duration;
use metrics::{histogram, increment};
use metrics_exporter_prometheus::PrometheusBuilder;
use quanta::Clock;
fn main() {
tracing_subscriber::fmt::init();
let builder = PrometheusBuilder::new();
builder
.install()
.expect("failed to install Prometheus recorder");
let clock = Clock::new();
let mut last = None;
loop {
increment!("tcp_server_loops", "system" => "foo");
if let Some(t) = last {
let delta: Duration = clock.now() - t;
histogram!("tcp_server_loop_delta_ns", delta, "system" => "foo");
}
last = Some(clock.now());
thread::sleep(Duration::from_millis(750));
}
}

View File

@ -0,0 +1,522 @@
//! Records metrics in the Prometheus exposition format.
#![deny(missing_docs)]
use std::future::Future;
use hyper::{
service::{make_service_fn, service_fn},
{Body, Error as HyperError, Response, Server},
};
use metrics::{Identifier, Key, Label, Recorder, SetRecorderError};
use metrics_util::{
parse_quantiles, CompositeKey, Handle, Histogram, MetricKind, Quantile, Registry,
};
use parking_lot::RwLock;
use std::io;
use std::iter::FromIterator;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::thread;
use std::{collections::HashMap, time::SystemTime};
use tokio::{pin, runtime, select};
type PrometheusRegistry = Registry<CompositeKey, Handle>;
type HdrHistogram = hdrhistogram::Histogram<u64>;
/// Errors that could occur while installing a Prometheus recorder/exporter.
#[derive(Debug)]
pub enum Error {
/// Creating the networking event loop did not succeed.
Io(io::Error),
/// Installing the recorder did not succeed.
Recorder(SetRecorderError),
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
impl From<SetRecorderError> for Error {
fn from(e: SetRecorderError) -> Self {
Error::Recorder(e)
}
}
#[derive(Clone)]
enum Distribution {
/// A Prometheus histogram.
///
/// Exposes "bucketed" values to Prometheus, counting the number of samples
/// below a given threshold i.e. 100 requests faster than 20ms, 1000 requests
/// faster than 50ms, etc.
Histogram(Histogram),
/// A Prometheus summary.
///
/// Computes and exposes value quantiles directly to Prometheus i.e. 50% of
/// requests were faster than 200ms, and 99% of requests were faster than
/// 1000ms, etc.
Summary(HdrHistogram, u64),
}
struct Snapshot {
pub counters: HashMap<String, HashMap<Vec<String>, u64>>,
pub gauges: HashMap<String, HashMap<Vec<String>, f64>>,
pub distributions: HashMap<String, HashMap<Vec<String>, Distribution>>,
}
struct Inner {
registry: PrometheusRegistry,
distributions: RwLock<HashMap<String, HashMap<Vec<String>, Distribution>>>,
quantiles: Vec<Quantile>,
buckets: Vec<u64>,
buckets_by_name: Option<HashMap<String, Vec<u64>>>,
}
impl Inner {
pub fn registry(&self) -> &PrometheusRegistry {
&self.registry
}
fn get_recent_metrics(&self) -> Snapshot {
let metrics = self.registry.get_handles();
let mut counters = HashMap::new();
let mut gauges = HashMap::new();
let mut sorted_overrides = self
.buckets_by_name
.as_ref()
.map(|h| Vec::from_iter(h.iter()))
.unwrap_or_else(|| vec![]);
sorted_overrides.sort_by(|(a, _), (b, _)| b.len().cmp(&a.len()));
for (key, handle) in metrics.into_iter() {
let (kind, key) = key.into_parts();
let (name, labels) = key_to_parts(key);
match kind {
MetricKind::Counter => {
let entry = counters
.entry(name)
.or_insert_with(|| HashMap::new())
.entry(labels)
.or_insert(0);
*entry = handle.read_counter();
}
MetricKind::Gauge => {
let entry = gauges
.entry(name)
.or_insert_with(|| HashMap::new())
.entry(labels)
.or_insert(0.0);
*entry = handle.read_gauge();
}
MetricKind::Histogram => {
let buckets = sorted_overrides
.iter()
.find(|(k, _)| name.ends_with(*k))
.map(|(_, buckets)| *buckets)
.unwrap_or(&self.buckets);
let mut wg = self.distributions.write();
let entry = wg
.entry(name.clone())
.or_insert_with(|| HashMap::new())
.entry(labels)
.or_insert_with(|| match buckets.is_empty() {
false => {
let histogram = Histogram::new(buckets)
.expect("failed to create histogram with buckets defined");
Distribution::Histogram(histogram)
}
true => {
let summary =
HdrHistogram::new(3).expect("failed to create histogram");
Distribution::Summary(summary, 0)
}
});
match entry {
Distribution::Histogram(histogram) => handle
.read_histogram_with_clear(|samples| histogram.record_many(samples)),
Distribution::Summary(summary, sum) => {
handle.read_histogram_with_clear(|samples| {
for sample in samples {
let _ = summary.record(*sample);
*sum += *sample;
}
})
}
}
}
}
}
let distributions = self.distributions.read().clone();
Snapshot {
counters,
gauges,
distributions,
}
}
pub fn render(&self) -> String {
let mut sorted_overrides = self
.buckets_by_name
.as_ref()
.map(|h| Vec::from_iter(h.iter()))
.unwrap_or_else(|| vec![]);
sorted_overrides.sort_by(|(a, _), (b, _)| b.len().cmp(&a.len()));
let Snapshot {
mut counters,
mut gauges,
mut distributions,
} = self.get_recent_metrics();
let ts = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let mut output = format!(
"# metrics snapshot (ts={}) (prometheus exposition format)",
ts
);
for (name, mut by_labels) in counters.drain() {
output.push_str("\n# TYPE ");
output.push_str(name.as_str());
output.push_str(" counter\n");
for (labels, value) in by_labels.drain() {
let full_name = render_labeled_name(&name, &labels);
output.push_str(full_name.as_str());
output.push_str(" ");
output.push_str(value.to_string().as_str());
output.push_str("\n");
}
}
for (name, mut by_labels) in gauges.drain() {
output.push_str("\n# TYPE ");
output.push_str(name.as_str());
output.push_str(" gauge\n");
for (labels, value) in by_labels.drain() {
let full_name = render_labeled_name(&name, &labels);
output.push_str(full_name.as_str());
output.push_str(" ");
output.push_str(value.to_string().as_str());
output.push_str("\n");
}
}
let mut sorted_overrides = self
.buckets_by_name
.as_ref()
.map(|h| Vec::from_iter(h.iter()))
.unwrap_or_else(|| vec![]);
sorted_overrides.sort_by(|(a, _), (b, _)| b.len().cmp(&a.len()));
for (name, mut by_labels) in distributions.drain() {
let has_buckets = sorted_overrides
.iter()
.any(|(k, _)| !self.buckets.is_empty() || name.ends_with(*k));
output.push_str("\n# TYPE ");
output.push_str(name.as_str());
output.push_str(" ");
output.push_str(if has_buckets { "histogram" } else { "summary" });
output.push_str("\n");
for (labels, distribution) in by_labels.drain() {
let (sum, count) = match distribution {
Distribution::Summary(summary, sum) => {
for quantile in &self.quantiles {
let value = summary.value_at_quantile(quantile.value());
let mut labels = labels.clone();
labels.push(format!("quantile=\"{}\"", quantile.value()));
let full_name = render_labeled_name(&name, &labels);
output.push_str(full_name.as_str());
output.push_str(" ");
output.push_str(value.to_string().as_str());
output.push_str("\n");
}
(sum, summary.len())
}
Distribution::Histogram(histogram) => {
for (le, count) in histogram.buckets() {
let mut labels = labels.clone();
labels.push(format!("le=\"{}\"", le));
let bucket_name = format!("{}_bucket", name);
let full_name = render_labeled_name(&bucket_name, &labels);
output.push_str(full_name.as_str());
output.push_str(" ");
output.push_str(count.to_string().as_str());
output.push_str("\n");
}
let mut labels = labels.clone();
labels.push("le=\"+Inf\"".to_owned());
let bucket_name = format!("{}_bucket", name);
let full_name = render_labeled_name(&bucket_name, &labels);
output.push_str(full_name.as_str());
output.push_str(" ");
output.push_str(histogram.count().to_string().as_str());
output.push_str("\n");
(histogram.sum(), histogram.count())
}
};
let sum_name = format!("{}_sum", name);
let full_sum_name = render_labeled_name(&sum_name, &labels);
output.push_str(full_sum_name.as_str());
output.push_str(" ");
output.push_str(sum.to_string().as_str());
output.push_str("\n");
let count_name = format!("{}_count", name);
let full_count_name = render_labeled_name(&count_name, &labels);
output.push_str(full_count_name.as_str());
output.push_str(" ");
output.push_str(count.to_string().as_str());
output.push_str("\n");
}
}
output
}
}
/// A Prometheus recorder.
pub struct PrometheusRecorder {
inner: Arc<Inner>,
}
/// Builder for creating and installing a Prometheus recorder/exporter.
pub struct PrometheusBuilder {
listen_address: SocketAddr,
quantiles: Vec<Quantile>,
buckets: Vec<u64>,
buckets_by_name: Option<HashMap<String, Vec<u64>>>,
}
impl PrometheusBuilder {
/// Creates a new [`PrometheusBuilder`].
pub fn new() -> Self {
let quantiles = parse_quantiles(&[0.0, 0.5, 0.9, 0.95, 0.99, 0.999, 1.0]);
Self {
listen_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 9000),
quantiles,
buckets: vec![],
buckets_by_name: None,
}
}
/// Sets the listen address for the Prometheus scrape endpoint.
///
/// The HTTP listener that is spawned will respond to GET requests on any request path.
///
/// Defaults to `127.0.0.1:9000`.
pub fn listen_address(mut self, addr: impl Into<SocketAddr>) -> Self {
self.listen_address = addr.into();
self
}
/// Sets the quantiles to use when rendering histograms.
///
/// Quantiles represent a scale of 0 to 1, where percentiles represent a scale of 1 to 100, so
/// a quantile of 0.99 is the 99th percentile, and a quantile of 0.99 is the 99.9th percentile.
///
/// By default, the quantiles will be set to: 0.0, 0.5, 0.9, 0.95, 0.99, 0.999, and 1.0. This means
/// that all histograms will be exposed as Prometheus summaries.
///
/// If buckets are set (via [`set_buckets`] or [`set_buckets_for_metric`]) then all histograms will
/// be exposed as summaries instead.
pub fn set_quantiles(mut self, quantiles: &[f64]) -> Self {
self.quantiles = parse_quantiles(quantiles);
self
}
/// Sets the buckets to use when rendering histograms.
///
/// Buckets values represent the higher bound of each buckets. If buckets are set, then all
/// histograms will be rendered as true Prometheus histograms, instead of summaries.
pub fn set_buckets(mut self, values: &[u64]) -> Self {
self.buckets = values.to_vec();
self
}
/// Sets the buckets for a specific metric, overidding the default.
///
/// The match is suffix-based, and the longest match found will be used.
///
/// Buckets values represent the higher bound of each buckets. If buckets are set, then any
/// histograms that match will be rendered as true Prometheus histograms, instead of summaries.
///
/// This option changes the observer's output of histogram-type metric into summaries.
/// It only affects matching metrics if set_buckets was not used.
pub fn set_buckets_for_metric(mut self, name: &str, values: &[u64]) -> Self {
let buckets = self.buckets_by_name.get_or_insert_with(|| HashMap::new());
buckets.insert(name.to_owned(), values.to_vec());
self
}
/// Builds the recorder and exporter and installs them globally.
///
/// An error will be returned if there's an issue with creating the HTTP server or with
/// installing the recorder as the global recorder.
pub fn install(self) -> Result<(), Error> {
let (recorder, exporter) = self.build();
metrics::set_boxed_recorder(Box::new(recorder))?;
let mut runtime = runtime::Builder::new()
.basic_scheduler()
.enable_all()
.build()?;
thread::Builder::new()
.name("metrics-exporter-prometheus-http".to_string())
.spawn(move || {
runtime.block_on(async move {
pin!(exporter);
loop {
select! {
_ = &mut exporter => {}
}
}
});
})?;
Ok(())
}
/// Builds the recorder and exporter and returns them both.
///
/// In most cases, users should prefer to use [`PrometheusBuilder::install`] to create and
/// install the recorder and exporter automatically for them. If a caller is combining
/// recorders, or needs to schedule the exporter to run in a particular way, this method
/// provides the flexibility to do so.
pub fn build(
self,
) -> (
PrometheusRecorder,
impl Future<Output = Result<(), HyperError>> + Send + Sync + 'static,
) {
let inner = Arc::new(Inner {
registry: Registry::new(),
distributions: RwLock::new(HashMap::new()),
quantiles: self.quantiles.clone(),
buckets: self.buckets.clone(),
buckets_by_name: self.buckets_by_name.clone(),
});
let recorder = PrometheusRecorder {
inner: inner.clone(),
};
let address = self.listen_address;
let exporter = async move {
let make_svc = make_service_fn(move |_| {
let inner = inner.clone();
async move {
Ok::<_, HyperError>(service_fn(move |_| {
let inner = inner.clone();
async move {
let output = inner.render();
Ok::<_, HyperError>(Response::new(Body::from(output)))
}
}))
}
});
Server::bind(&address).serve(make_svc).await
};
(recorder, exporter)
}
}
impl Recorder for PrometheusRecorder {
fn register_counter(&self, key: Key, _description: Option<&'static str>) -> Identifier {
self.inner
.registry()
.get_or_create_identifier(CompositeKey::new(MetricKind::Counter, key), |_| {
Handle::counter()
})
}
fn register_gauge(&self, key: Key, _description: Option<&'static str>) -> Identifier {
self.inner
.registry()
.get_or_create_identifier(CompositeKey::new(MetricKind::Gauge, key), |_| {
Handle::gauge()
})
}
fn register_histogram(&self, key: Key, _description: Option<&'static str>) -> Identifier {
self.inner
.registry()
.get_or_create_identifier(CompositeKey::new(MetricKind::Histogram, key), |_| {
Handle::histogram()
})
}
fn increment_counter(&self, id: Identifier, value: u64) {
self.inner
.registry()
.with_handle(id, |h| h.increment_counter(value));
}
fn update_gauge(&self, id: Identifier, value: f64) {
self.inner
.registry()
.with_handle(id, |h| h.update_gauge(value));
}
fn record_histogram(&self, id: Identifier, value: u64) {
self.inner
.registry()
.with_handle(id, |h| h.record_histogram(value));
}
}
fn key_to_parts(key: Key) -> (String, Vec<String>) {
let (name, labels) = key.into_parts();
let sanitize = |c| c == '.' || c == '=' || c == '{' || c == '}' || c == '+' || c == '-';
let name = name.replace(sanitize, "_");
let labels = labels
.into_iter()
.map(Label::into_parts)
.map(|(k, v)| {
format!(
"{}=\"{}\"",
k,
v.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
)
})
.collect();
(name, labels)
}
fn render_labeled_name(name: &str, labels: &[String]) -> String {
let mut output = name.to_string();
if !labels.is_empty() {
let joined = labels.join(",");
output.push_str("{");
output.push_str(&joined);
output.push_str("}");
}
output
}

View File

@ -5,11 +5,19 @@ authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
license = "MIT"
description = "Emits metrics data to clients over TCP."
description = "A metrics-compatible exporter that outputs metrics to clients over TCP."
homepage = "https://github.com/metrics-rs/metrics"
repository = "https://github.com/metrics-rs/metrics"
documentation = "https://docs.rs/metrics-exporter-tcp"
readme = "README.md"
categories = ["development-tools::debugging"]
keywords = ["metrics", "telemetry", "tcp"]
[dependencies]
metrics = { version = "^0.12", path = "../metrics", features = ["std"] }
metrics-util = { version = "^0.3", path = "../metrics-util" }
metrics = { version = "^0.13", path = "../metrics", features = ["std"] }
metrics-util = { version = "^0.4", path = "../metrics-util" }
bytes = "^0.5"
crossbeam-channel = "^0.4"
prost = "^0.6"

View File

@ -19,7 +19,7 @@ fn main() {
match stream.read(&mut rbuf[..]) {
Ok(0) => {
println!("server disconnected, closing");
break
break;
}
Ok(n) => buf.put_slice(&rbuf[..n]),
Err(e) => eprintln!("read error: {:?}", e),

View File

@ -20,7 +20,7 @@ fn main() {
if let Some(t) = last {
let delta: Duration = clock.now() - t;
histogram!("tcp_server_loop_delta_ns", delta.as_secs_f64(), "system" => "foo");
histogram!("tcp_server_loop_delta_ns", delta, "system" => "foo");
}
last = Some(clock.now());

View File

@ -24,5 +24,5 @@ message Gauge {
}
message Histogram {
double value = 1;
uint64 value = 1;
}

View File

@ -37,7 +37,7 @@ enum MetricKind {
enum MetricValue {
Counter(u64),
Gauge(f64),
Histogram(f64),
Histogram(u64),
}
#[derive(Eq, PartialEq, Hash, Clone)]
@ -49,10 +49,13 @@ impl CompositeKey {
}
}
// Errors that could occur while install a TCP recorder/exporter.
// Errors that could occur while installing a TCP recorder/exporter.
#[derive(Debug)]
pub enum Error {
// Creating the networking event loop did not succeed.
Io(io::Error),
// Installing the recorder did not succeed.
Recorder(SetRecorderError),
}
@ -93,7 +96,7 @@ impl TcpBuilder {
///
/// The exporter will accept connections on this address and immediately begin forwarding
/// metrics to the client.
///
///
/// Defaults to `127.0.0.1:5000`.
pub fn listen_address<A>(mut self, addr: A) -> TcpBuilder
where
@ -104,15 +107,15 @@ impl TcpBuilder {
}
/// Sets the buffer size for internal operations.
///
///
/// The buffer size controls two operational aspects: the number of metrics processed
/// per iteration of the event loop, and the number of buffered metrics each client
/// can hold.
///
/// can hold.
///
/// This setting allows trading off responsiveness for throughput, where a smaller buffer
/// size will ensure that metrics are pushed to clients sooner, versus a larger buffer
/// size that allows us to push more at a time.alloc
///
///
/// As well, the larger the buffer, the more messages a client can temporarily hold.
/// Clients have a circular buffer implementation so if their buffers are full, metrics
/// will be dropped as necessary to avoid backpressure in the recorder.
@ -122,7 +125,7 @@ impl TcpBuilder {
}
/// Installs the recorder and exporter.
///
///
/// An error will be returned if there's an issue with creating the TCP server or with
/// installing the recorder as the global recorder.
pub fn install(self) -> Result<(), Error> {
@ -186,7 +189,7 @@ impl Recorder for TcpRecorder {
self.push_metric(id, MetricValue::Gauge(value));
}
fn record_histogram(&self, id: Identifier, value: f64) {
fn record_histogram(&self, id: Identifier, value: u64) {
self.push_metric(id, MetricValue::Histogram(value));
}
}

View File

@ -4,7 +4,16 @@ version = "0.1.0"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
license = "MIT"
description = "Macros for the metrics crate."
homepage = "https://github.com/metrics-rs/metrics"
repository = "https://github.com/metrics-rs/metrics"
documentation = "https://docs.rs/metrics"
readme = "README.md"
categories = ["development-tools::debugging"]
keywords = ["metrics", "facade", "macros"]
[lib]
proc-macro = true

View File

@ -25,7 +25,7 @@ struct WithExpression {
struct Registration {
key: Key,
desc: LitStr,
desc: Option<LitStr>,
labels: Vec<(LitStr, Expr)>,
}
@ -80,10 +80,26 @@ impl Parse for Registration {
fn parse(mut input: ParseStream) -> Result<Self> {
let key = read_key(&mut input)?;
input.parse::<Token![,]>()?;
let desc: LitStr = input.parse()?;
// This may or may not be the start of labels, if the description has been omitted, so
// we hold on to it until we can make sure nothing else is behind it, or if it's a full
// fledged set of labels.
let mut possible_desc = if input.parse::<Token![,]>().is_ok() {
input.parse().ok()
} else {
None
};
let mut labels = Vec::new();
// Try and parse a single label by hand in case the caller omitted the description.
if possible_desc.is_some() && input.parse::<Token![=>]>().is_ok() {
if let Ok(lvalue) = input.parse::<Expr>() {
// We've matched "key => value" at this point, so clearly it wasn't a description,
// so let's add this label and then continue on with the loop to parse any more labels.
labels.push((possible_desc.take().unwrap(), lvalue));
}
}
loop {
if input.is_empty() {
break;
@ -95,7 +111,7 @@ impl Parse for Registration {
labels.push((lkey, lvalue));
}
Ok(Registration { key, desc, labels })
Ok(Registration { key, desc: possible_desc, labels })
}
}
@ -165,36 +181,29 @@ pub fn histogram(input: TokenStream) -> TokenStream {
fn get_expanded_registration(
metric_type: &str,
key: Key,
desc: LitStr,
desc: Option<LitStr>,
labels: Vec<(LitStr, Expr)>,
) -> TokenStream {
let register_ident = format_ident!("register_{}", metric_type);
let key = match key {
Key::NotScoped(s) => {
quote! { #s }
}
Key::Scoped(s) => {
quote! {
format!("{}.{}", std::module_path!().replace("::", "."), #s)
}
}
};
let key = key_to_quoted(key);
let insertable_labels = labels
.into_iter()
.map(|(k, v)| quote! { metrics::Label::new(#k, #v) });
let desc = match desc {
Some(desc) => quote! { Some(#desc) },
None => quote! { None },
};
let expanded = quote! {
{
// Only do this work if there's a recorder installed.
if let Some(recorder) = metrics::try_recorder() {
let mlabels = vec![#(#insertable_labels),*];
recorder.#register_ident((#key, mlabels).into(), Some(#desc));
recorder.#register_ident((#key, mlabels).into(), #desc);
}
}
};
debug_tokens(&expanded);
TokenStream::from(expanded)
}
@ -210,16 +219,7 @@ where
{
let register_ident = format_ident!("register_{}", metric_type);
let op_ident = format_ident!("{}_{}", op_type, metric_type);
let key = match key {
Key::NotScoped(s) => {
quote! { #s }
}
Key::Scoped(s) => {
quote! {
format!("{}.{}", std::module_path!().replace("::", "."), #s)
}
}
};
let key = key_to_quoted(key);
let use_fast_path = can_use_fast_path(&labels);
let composite_key = if labels.is_empty() {
@ -231,6 +231,14 @@ where
quote! { (#key, vec![#(#insertable_labels),*]).into() }
};
let op_values = if metric_type == "histogram" {
quote! {
metrics::__into_u64(#op_values)
}
} else {
quote! { #op_values }
};
let expanded = if use_fast_path {
// We're on the fast path here, so we'll end up registering with the recorder
// and statically caching the identifier for our metric to speed up any future
@ -266,8 +274,6 @@ where
}
};
debug_tokens(&expanded);
TokenStream::from(expanded)
}
@ -282,6 +288,19 @@ fn read_key(input: &mut ParseStream) -> Result<Key> {
}
}
fn key_to_quoted(key: Key) -> proc_macro2::TokenStream {
match key {
Key::NotScoped(s) => {
quote! { #s }
},
Key::Scoped(s) => {
quote! {
format!("{}.{}", std::module_path!().replace("::", "."), #s)
}
},
}
}
fn can_use_fast_path(labels: &[(LitStr, Expr)]) -> bool {
let mut use_fast_path = true;
for (_, lvalue) in labels {
@ -293,22 +312,4 @@ fn can_use_fast_path(labels: &[(LitStr, Expr)]) -> bool {
}
}
use_fast_path
}
#[rustversion::nightly]
fn debug_tokens<T: ToTokens>(tokens: &T) {
if std::env::var_os("METRICS_DEBUG").is_some() {
let ts = tokens.into_token_stream();
proc_macro::Span::call_site()
.note("emitting metrics macro debug output")
.note(ts.to_string())
.emit()
}
}
#[rustversion::not(nightly)]
fn debug_tokens<T: ToTokens>(_tokens: &T) {
if std::env::var_os("METRICS_DEBUG").is_some() {
eprintln!("nightly required to output proc macro diagnostics!");
}
}
}

View File

@ -1,21 +0,0 @@
[package]
name = "metrics-observer-prometheus"
version = "0.1.3"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
license = "MIT"
description = "A metrics-core compatible observer that outputs the Prometheus exposition output."
homepage = "https://github.com/metrics-rs/metrics"
repository = "https://github.com/metrics-rs/metrics"
documentation = "https://docs.rs/metrics-observer-prometheus"
readme = "README.md"
categories = ["development-tools::debugging"]
keywords = ["metrics", "telemetry", "prometheus"]
[dependencies]
metrics-core = { path = "../metrics-core", version = "^0.5" }
metrics-util = { path = "../metrics-util", version = "^0.3" }
hdrhistogram = { version = "^6.3", default-features = false }

View File

@ -1,219 +0,0 @@
//! Records metrics in the Prometheus exposition format.
#![deny(missing_docs)]
use hdrhistogram::Histogram;
use metrics_core::{Builder, Drain, Key, Label, Observer};
use metrics_util::{parse_quantiles, Quantile};
use std::{collections::HashMap, time::SystemTime};
/// Builder for [`PrometheusObserver`].
pub struct PrometheusBuilder {
quantiles: Vec<Quantile>,
}
impl PrometheusBuilder {
/// Creates a new [`PrometheusBuilder`] with default values.
pub fn new() -> Self {
let quantiles = parse_quantiles(&[0.0, 0.5, 0.9, 0.95, 0.99, 0.999, 1.0]);
Self { quantiles }
}
/// Sets the quantiles to use when rendering histograms.
///
/// Quantiles represent a scale of 0 to 1, where percentiles represent a scale of 1 to 100, so
/// a quantile of 0.99 is the 99th percentile, and a quantile of 0.99 is the 99.9th percentile.
///
/// By default, the quantiles will be set to: 0.0, 0.5, 0.9, 0.95, 0.99, 0.999, and 1.0.
pub fn set_quantiles(mut self, quantiles: &[f64]) -> Self {
self.quantiles = parse_quantiles(quantiles);
self
}
}
impl Builder for PrometheusBuilder {
type Output = PrometheusObserver;
fn build(&self) -> Self::Output {
PrometheusObserver {
quantiles: self.quantiles.clone(),
histos: HashMap::new(),
output: get_prom_expo_header(),
counters: HashMap::new(),
gauges: HashMap::new(),
}
}
}
impl Default for PrometheusBuilder {
fn default() -> Self {
Self::new()
}
}
/// Records metrics in the Prometheus exposition format.
pub struct PrometheusObserver {
pub(crate) quantiles: Vec<Quantile>,
pub(crate) histos: HashMap<String, HashMap<Vec<String>, (u64, Histogram<u64>)>>,
pub(crate) output: String,
pub(crate) counters: HashMap<String, HashMap<Vec<String>, u64>>,
pub(crate) gauges: HashMap<String, HashMap<Vec<String>, i64>>,
}
impl Observer for PrometheusObserver {
fn observe_counter(&mut self, key: Key, value: u64) {
let (name, labels) = key_to_parts(key);
let entry = self
.counters
.entry(name)
.or_insert_with(|| HashMap::new())
.entry(labels)
.or_insert_with(|| 0);
*entry += value;
}
fn observe_gauge(&mut self, key: Key, value: i64) {
let (name, labels) = key_to_parts(key);
let entry = self
.gauges
.entry(name)
.or_insert_with(|| HashMap::new())
.entry(labels)
.or_insert_with(|| 0);
*entry = value;
}
fn observe_histogram(&mut self, key: Key, values: &[u64]) {
let (name, labels) = key_to_parts(key);
let entry = self
.histos
.entry(name)
.or_insert_with(|| HashMap::new())
.entry(labels)
.or_insert_with(|| {
let h = Histogram::<u64>::new(3).expect("failed to create histogram");
(0, h)
});
let (sum, h) = entry;
for value in values {
h.record(*value).expect("failed to observe histogram value");
*sum += *value;
}
}
}
impl Drain<String> for PrometheusObserver {
fn drain(&mut self) -> String {
let mut output: String = self.output.drain(..).collect();
for (name, mut by_labels) in self.counters.drain() {
output.push_str("\n# TYPE ");
output.push_str(name.as_str());
output.push_str(" counter\n");
for (labels, value) in by_labels.drain() {
let full_name = render_labeled_name(&name, &labels);
output.push_str(full_name.as_str());
output.push_str(" ");
output.push_str(value.to_string().as_str());
output.push_str("\n");
}
}
for (name, mut by_labels) in self.gauges.drain() {
output.push_str("\n# TYPE ");
output.push_str(name.as_str());
output.push_str(" gauge\n");
for (labels, value) in by_labels.drain() {
let full_name = render_labeled_name(&name, &labels);
output.push_str(full_name.as_str());
output.push_str(" ");
output.push_str(value.to_string().as_str());
output.push_str("\n");
}
}
for (name, mut by_labels) in self.histos.drain() {
output.push_str("\n# TYPE ");
output.push_str(name.as_str());
output.push_str(" summary\n");
for (labels, sh) in by_labels.drain() {
let (sum, hist) = sh;
for quantile in &self.quantiles {
let value = hist.value_at_quantile(quantile.value());
let mut labels = labels.clone();
labels.push(format!("quantile=\"{}\"", quantile.value()));
let full_name = render_labeled_name(&name, &labels);
output.push_str(full_name.as_str());
output.push_str(" ");
output.push_str(value.to_string().as_str());
output.push_str("\n");
}
let sum_name = format!("{}_sum", name);
let full_sum_name = render_labeled_name(&sum_name, &labels);
output.push_str(full_sum_name.as_str());
output.push_str(" ");
output.push_str(sum.to_string().as_str());
output.push_str("\n");
let count_name = format!("{}_count", name);
let full_count_name = render_labeled_name(&count_name, &labels);
output.push_str(full_count_name.as_str());
output.push_str(" ");
output.push_str(hist.len().to_string().as_str());
output.push_str("\n");
}
}
output
}
}
fn key_to_parts(key: Key) -> (String, Vec<String>) {
let (name, labels) = key.into_parts();
let sanitize = |c| c == '.' || c == '=' || c == '{' || c == '}' || c == '+' || c == '-';
let name = name.replace(sanitize, "_");
let labels = labels
.into_iter()
.map(Label::into_parts)
.map(|(k, v)| {
format!(
"{}=\"{}\"",
k,
v.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
)
})
.collect();
(name, labels)
}
fn render_labeled_name(name: &str, labels: &[String]) -> String {
let mut output = name.to_string();
if !labels.is_empty() {
let joined = labels.join(",");
output.push_str("{");
output.push_str(&joined);
output.push_str("}");
}
output
}
fn get_prom_expo_header() -> String {
let ts = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
format!(
"# metrics snapshot (ts={}) (prometheus exposition format)",
ts
)
}

View File

@ -1,6 +1,6 @@
[package]
name = "metrics-util"
version = "0.3.1"
version = "0.4.0"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
@ -28,7 +28,7 @@ name = "streaming_integers"
harness = false
[dependencies]
metrics = { path = "../metrics", version = "^0.12", features = ["std"] }
metrics = { version = "^0.13", path = "../metrics", features = ["std"] }
crossbeam-epoch = "^0.8"
crossbeam-utils = "^0.7"
serde = "^1.0"

View File

@ -5,7 +5,7 @@ use crate::{handle::Handle, registry::Registry};
use metrics::{Identifier, Key, Recorder};
/// Metric kinds.
#[derive(Eq, PartialEq, Hash, Clone)]
#[derive(Eq, PartialEq, Hash, Clone, Copy)]
pub enum MetricKind {
/// Counter.
Counter,
@ -33,7 +33,7 @@ pub enum DebugValue {
/// Gauge.
Gauge(f64),
/// Histogram.
Histogram(Vec<f64>),
Histogram(Vec<u64>),
}
/// Captures point-in-time snapshots of `DebuggingRecorder`.
@ -117,7 +117,7 @@ impl Recorder for DebuggingRecorder {
.with_handle(id, |handle| handle.update_gauge(value));
}
fn record_histogram(&self, id: Identifier, value: f64) {
fn record_histogram(&self, id: Identifier, value: u64) {
self.registry
.with_handle(id, |handle| handle.record_histogram(value));
}

View File

@ -17,7 +17,7 @@ pub enum Handle {
Gauge(Arc<AtomicU64>),
/// A histogram.
Histogram(Arc<AtomicBucket<f64>>),
Histogram(Arc<AtomicBucket<u64>>),
}
impl Handle {
@ -68,7 +68,7 @@ impl Handle {
/// Records to this handle as a histogram.
///
/// Panics if this handle is not a histogram.
pub fn record_histogram(&self, value: f64) {
pub fn record_histogram(&self, value: u64) {
match self {
Handle::Histogram(bucket) => bucket.push(value),
_ => panic!("tried to record as histogram"),
@ -101,10 +101,26 @@ impl Handle {
/// Reads this handle as a histogram.
///
/// Panics if this handle is not a histogram.
pub fn read_histogram(&self) -> Vec<f64> {
pub fn read_histogram(&self) -> Vec<u64> {
match self {
Handle::Histogram(bucket) => bucket.data(),
_ => panic!("tried to read as histogram"),
}
}
/// Reads this handle as a histogram incrementally into a closure, and clears the histogram.
///
/// The closure `f` passed in is invoked multiple times with slices of values present in the
/// histogram currently. Once all values have been read, the histogram is cleared of all values.
///
/// Panics if this handle is not a histogram.
pub fn read_histogram_with_clear<F>(&self, f: F)
where
F: FnMut(&[u64]),
{
match self {
Handle::Histogram(bucket) => bucket.clear_with(f),
_ => panic!("tried to read as histogram"),
}
}
}

View File

@ -0,0 +1,147 @@
//! Helper functions and types related to histogram data.
/// A bucketed histogram.
///
/// This histogram tracks the number of samples that fall into pre-defined buckets,
/// rather than exposing any sort of quantiles.
///
/// This type is most useful with systems that prefer bucketed data, such as Prometheus'
/// histogram type, as opposed to its summary type, which deals with quantiles.
#[derive(Debug, Clone)]
pub struct Histogram {
count: u64,
bounds: Vec<u64>,
buckets: Vec<u64>,
sum: u64,
}
impl Histogram {
/// Creates a new `Histogram`.
///
/// If `bounds` is empty, returns `None`.
pub fn new(bounds: &[u64]) -> Option<Histogram> {
if bounds.len() == 0 {
return None;
}
let mut buckets = Vec::with_capacity(bounds.len());
for _ in bounds {
buckets.push(0);
}
Some(Histogram {
count: 0,
bounds: Vec::from(bounds),
buckets,
sum: 0,
})
}
/// Gets the sum of all samples.
pub fn sum(&self) -> u64 {
self.sum
}
/// Gets the sample count.
pub fn count(&self) -> u64 {
self.count
}
/// Gets the buckets.
///
/// Buckets are tuples, where the first element is the bucket limit itself, and the second
/// element is the count of samples in that bucket.
pub fn buckets(&self) -> Vec<(u64, u64)> {
self.bounds
.iter()
.cloned()
.zip(self.buckets.iter().cloned())
.collect()
}
/// Records a single sample.
pub fn record(&mut self, sample: u64) {
self.sum += sample;
self.count += 1;
// Add the sample to every bucket where the value is less than the bound.
for (idx, bucket) in self.bounds.iter().enumerate() {
if sample <= *bucket {
self.buckets[idx] += 1;
}
}
}
/// Records multiple samples.
pub fn record_many<'a, S>(&mut self, samples: S)
where
S: IntoIterator<Item = &'a u64> + 'a,
{
let mut bucketed = Vec::with_capacity(self.buckets.len());
for _ in 0..self.buckets.len() {
bucketed.push(0);
}
let mut sum = 0;
let mut count = 0;
for sample in samples.into_iter() {
sum += *sample;
count += 1;
for (idx, bucket) in self.bounds.iter().enumerate() {
if sample <= bucket {
bucketed[idx] += 1;
break;
}
}
}
// Add each bucket to the next bucket to satisfy the "less than or equal to"
// behavior of the buckets.
if bucketed.len() >= 2 {
for idx in 0..(bucketed.len() - 1) {
bucketed[idx + 1] += bucketed[idx];
}
}
// Merge our temporary buckets to our main buckets.
for (idx, local) in bucketed.iter().enumerate() {
self.buckets[idx] += local;
}
self.sum += sum;
self.count += count;
}
}
#[cfg(test)]
mod tests {
use super::Histogram;
#[test]
fn test_histogram() {
// No buckets, can't do shit.
let histogram = Histogram::new(&[]);
assert!(histogram.is_none());
let buckets = &[10, 25, 100];
let values = vec![3, 2, 6, 12, 56, 82, 202, 100, 29];
let mut histogram = Histogram::new(buckets).expect("histogram should have been created");
histogram.record_many(&values);
histogram.record(89);
let result = histogram.buckets();
assert_eq!(result.len(), 3);
let (_, first) = result[0];
assert_eq!(first, 3);
let (_, second) = result[1];
assert_eq!(second, 4);
let (_, third) = result[2];
assert_eq!(third, 9);
assert_eq!(histogram.count(), values.len() as u64 + 1);
assert_eq!(histogram.sum(), 581);
}
}

37
metrics-util/src/key.rs Normal file
View File

@ -0,0 +1,37 @@
use crate::MetricKind;
use metrics::Key;
/// A composite key that stores both the metric key and the metric kind.
///
/// This type is intended to be used by recorders that internally take advantage of
/// the provided [`Registry`](crate::registry::Registry) type when also using
/// [`Handle`](crate::handle::Handle).
///
/// Since handles are opaque, having the standard key by itself could lead to two
/// different metric kinds tryin to use the same key, calling read or write methods
/// that inevitably panic. With `CompositeKey`, the kind can tied to the underlying
/// handle, ensuring parity between the two.
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct CompositeKey(MetricKind, Key);
impl CompositeKey {
/// Creates a new `CompositeKey`.
pub const fn new(kind: MetricKind, key: Key) -> CompositeKey {
CompositeKey(kind, key)
}
/// Gets the inner key represented by this `CompositeKey`.
pub fn key(&self) -> &Key {
&self.1
}
/// Gets the inner kind represented by this `CompositeKey`.
pub fn kind(&self) -> MetricKind {
self.0
}
/// Takes the individual pieces of this `CompositeKey`.
pub fn into_parts(self) -> (MetricKind, Key) {
(self.0, self.1)
}
}

View File

@ -20,3 +20,9 @@ pub use tree::{Integer, MetricsTree};
mod registry;
pub use registry::Registry;
mod key;
pub use key::CompositeKey;
mod histogram;
pub use histogram::Histogram;

View File

@ -1,6 +1,6 @@
[package]
name = "metrics"
version = "0.12.1"
version = "0.13.0"
authors = ["Toby Lawrence <toby@nuclearfurnace.com>"]
edition = "2018"
@ -25,7 +25,7 @@ name = "macros"
harness = false
[dependencies]
metrics-macros = { path = "../metrics-macros", version = "^0.1" }
metrics-macros = { version = "^0.1", path = "../metrics-macros" }
proc-macro-hack = "^0.5"
[dev-dependencies]

View File

@ -43,7 +43,7 @@ impl Recorder for PrintRecorder {
println!("(gauge) got value {} for id {}", value, uid);
}
fn record_histogram(&self, id: Identifier, value: f64) {
fn record_histogram(&self, id: Identifier, value: u64) {
let uid: usize = id.into();
println!("(histogram) got value {} for id {}", value, uid);
}
@ -75,10 +75,10 @@ fn main() {
gauge!("connection_count", 300.0);
gauge!("connection_count", 300.0, "listener" => "frontend");
gauge!("connection_count", 300.0, "listener" => "frontend", "server" => server_name.clone());
histogram!("service.execution_time", 70.0);
histogram!("service.execution_time", 70.0, "type" => "users");
histogram!("service.execution_time", 70.0, "type" => "users", "server" => server_name.clone());
histogram!(<"service.execution_time">, 70.0);
histogram!(<"service.execution_time">, 70.0, "type" => "users");
histogram!(<"service.execution_time">, 70.0, "type" => "users", "server" => server_name.clone());
histogram!("service.execution_time", 70);
histogram!("service.execution_time", 70, "type" => "users");
histogram!("service.execution_time", 70, "type" => "users", "server" => server_name.clone());
histogram!(<"service.execution_time">, 70);
histogram!(<"service.execution_time">, 70, "type" => "users");
histogram!(<"service.execution_time">, 70, "type" => "users", "server" => server_name.clone());
}

View File

@ -79,3 +79,30 @@ impl OnceIdentifier {
}
unsafe impl Sync for OnceIdentifier {}
/// An object which can be converted into a `u64` representation.
///
/// This trait provides a mechanism for existing types, which have a natural representation
/// as an unsigned 64-bit integer, to be transparently passed in when recording a histogram.
pub trait IntoU64 {
/// Converts this object to its `u64` representation.
fn into_u64(self) -> u64;
}
impl IntoU64 for u64 {
fn into_u64(self) -> u64 {
self
}
}
impl IntoU64 for std::time::Duration {
fn into_u64(self) -> u64 {
self.as_nanos() as u64
}
}
/// Helper method to allow monomorphization of values passed to the `histogram!` macro.
#[doc(hidden)]
pub fn __into_u64<V: IntoU64>(value: V) -> u64 {
value.into_u64()
}

View File

@ -8,16 +8,9 @@
//! 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.
//! The basic use of the facade crate is through the three metrics macros: [`counter!`], [`gauge!`],
//! and [`histogram!`]. These macros correspond to updating a counter, updating a gauge,
//! and updating a histogram.
//!
//! ## In libraries
//! Libraries should link only to the `metrics` crate, and use the provided macros to record
@ -35,7 +28,7 @@
//! let row_count = run_query(query);
//! let delta = Instant::now() - start;
//!
//! histogram!("process.query_time", delta.as_secs_f64());
//! histogram!("process.query_time", delta);
//! counter!("process.query_row_count", row_count);
//!
//! row_count
@ -58,7 +51,8 @@
//! # Available metrics implementations
//!
//! * # Native recorder:
//! * [metrics-runtime]
//! * [metrics-exporter-tcp] - outputs metrics to clients over TCP
//! * [metrics-exporter-prometheus] - serves a Prometheus scrape endpoint
//!
//! # Implementing a Recorder
//!
@ -114,7 +108,7 @@
//! info!("gauge '{}' -> {}", key, value);
//! }
//!
//! fn record_histogram(&self, id: Identifier, value: f64) {
//! fn record_histogram(&self, id: Identifier, value: u64) {
//! let key = self.get_key(id);
//! info!("histogram '{}' -> {}", key, value);
//! }
@ -134,7 +128,7 @@
//! # fn register_histogram(&self, _key: Key, _description: Option<&'static str>) -> Identifier { Identifier::default() }
//! # fn increment_counter(&self, _id: Identifier, _value: u64) {}
//! # fn update_gauge(&self, _id: Identifier, _value: f64) {}
//! # fn record_histogram(&self, _id: Identifier, _value: f64) {}
//! # fn record_histogram(&self, _id: Identifier, _value: u64) {}
//! # }
//! use metrics::SetRecorderError;
//!
@ -162,7 +156,7 @@
//! # fn register_histogram(&self, _key: Key, _description: Option<&'static str>) -> Identifier { Identifier::default() }
//! # fn increment_counter(&self, _id: Identifier, _value: u64) {}
//! # fn update_gauge(&self, _id: Identifier, _value: f64) {}
//! # fn record_histogram(&self, _id: Identifier, _value: f64) {}
//! # fn record_histogram(&self, _id: Identifier, _value: u64) {}
//! # }
//! use metrics::SetRecorderError;
//!
@ -173,7 +167,8 @@
//! # fn main() {}
//! ```
//!
//! [metrics-runtime]: https://docs.rs/metrics-runtime
//! [metrics-exporter-tcp]: https://docs.rs/metrics-exporter-tcp
//! [metrics-exporter-prometheus]: https://docs.rs/metrics-exporter-prometheus
#![deny(missing_docs)]
use proc_macro_hack::proc_macro_hack;
@ -193,29 +188,245 @@ mod macros;
pub use self::macros::*;
/// Registers a counter.
///
/// Counters represent a single value that can only be incremented over time, or reset to zero.
///
/// Metrics can be registered with an optional description. Whether or not the installed recorder
/// does anything with the description is implementation defined. Labels can also be specified
/// when registering a metric.
///
/// Counters, when registered, start at zero.
///
/// # Scoped versus unscoped
/// Metrics can be unscoped or scoped, where the scoping is derived by the current module the call
/// is taking place in. This scope is used as a prefix to the provided metric name.
///
/// # Example
/// ```
/// # use metrics::register_counter;
/// # fn main() {
/// // A regular, unscoped counter:
/// register_counter!("some_metric_name");
///
/// // A scoped counter. This inherits a scope derived by the current module:
/// register_counter!(<"some_metric_name">);
///
/// // Providing a description for a counter:
/// register_counter!("some_metric_name", "number of woopsy daisies");
///
/// // Specifying labels:
/// register_counter!("some_metric_name", "service" => "http");
///
/// // And all combined:
/// register_counter!("some_metric_name", "number of woopsy daisies", "service" => "http");
/// register_counter!(<"some_metric_name">, "number of woopsy daisies", "service" => "http");
/// # }
/// ```
#[proc_macro_hack]
pub use metrics_macros::register_counter;
/// Registers a gauge.
///
/// Gauges represent a single value that can go up or down over time.
///
/// Metrics can be registered with an optional description. Whether or not the installed recorder
/// does anything with the description is implementation defined. Labels can also be specified
/// when registering a metric.
///
/// Gauges, when registered, start at zero.
///
/// # Scoped versus unscoped
/// Metrics can be unscoped or scoped, where the scoping is derived by the current module the call
/// is taking place in. This scope is used as a prefix to the provided metric name.
///
/// # Example
/// ```
/// # use metrics::register_gauge;
/// # fn main() {
/// // A regular, unscoped gauge:
/// register_gauge!("some_metric_name");
///
/// // A scoped gauge. This inherits a scope derived by the current module:
/// register_gauge!(<"some_metric_name">);
///
/// // Providing a description for a gauge:
/// register_gauge!("some_metric_name", "number of woopsy daisies");
///
/// // Specifying labels:
/// register_gauge!("some_metric_name", "service" => "http");
///
/// // And all combined:
/// register_gauge!("some_metric_name", "number of woopsy daisies", "service" => "http");
/// register_gauge!(<"some_metric_name">, "number of woopsy daisies", "service" => "http");
/// # }
/// ```
#[proc_macro_hack]
pub use metrics_macros::register_gauge;
/// Registers a histogram.
/// Records a histogram.
///
/// Histograms measure the distribution of values for a given set of measurements.
///
/// Metrics can be registered with an optional description. Whether or not the installed recorder
/// does anything with the description is implementation defined. Labels can also be specified
/// when registering a metric.
///
/// Histograms, when registered, start at zero.
///
/// # Scoped versus unscoped
/// Metrics can be unscoped or scoped, where the scoping is derived by the current module the call
/// is taking place in. This scope is used as a prefix to the provided metric name.
///
/// # Example
/// ```
/// # use metrics::register_histogram;
/// # fn main() {
/// // A regular, unscoped histogram:
/// register_histogram!("some_metric_name");
///
/// // A scoped histogram. This inherits a scope derived by the current module:
/// register_histogram!(<"some_metric_name">);
///
/// // Providing a description for a histogram:
/// register_histogram!("some_metric_name", "number of woopsy daisies");
///
/// // Specifying labels:
/// register_histogram!("some_metric_name", "service" => "http");
///
/// // And all combined:
/// register_histogram!("some_metric_name", "number of woopsy daisies", "service" => "http");
/// register_histogram!(<"some_metric_name">, "number of woopsy daisies", "service" => "http");
/// # }
/// ```
#[proc_macro_hack]
pub use metrics_macros::register_histogram;
/// Increments a counter.
///
/// Counters represent a single value that can only be incremented over time, or reset to zero.
///
/// # Scoped versus unscoped
/// Metrics can be unscoped or scoped, where the scoping is derived by the current module the call
/// is taking place in. This scope is used as a prefix to the provided metric name.
///
/// # Example
/// ```
/// # use metrics::increment;
/// # fn main() {
/// // A regular, unscoped increment:
/// increment!("some_metric_name");
///
/// // A scoped increment. This inherits a scope derived by the current module:
/// increment!(<"some_metric_name">);
///
/// // Specifying labels:
/// increment!("some_metric_name", "service" => "http");
///
/// // And all combined:
/// increment!("some_metric_name", "service" => "http");
/// increment!(<"some_metric_name">, "service" => "http");
/// # }
/// ```
#[proc_macro_hack]
pub use metrics_macros::increment;
/// Increments a counter.
///
/// Counters represent a single value that can only be incremented over time, or reset to zero.
///
/// # Scoped versus unscoped
/// Metrics can be unscoped or scoped, where the scoping is derived by the current module the call
/// is taking place in. This scope is used as a prefix to the provided metric name.
///
/// # Example
/// ```
/// # use metrics::counter;
/// # fn main() {
/// // A regular, unscoped counter:
/// counter!("some_metric_name", 12);
///
/// // A scoped counter. This inherits a scope derived by the current module:
/// counter!(<"some_metric_name">, 12);
///
/// // Specifying labels:
/// counter!("some_metric_name", 12, "service" => "http");
///
/// // And all combined:
/// counter!("some_metric_name", 12, "service" => "http");
/// counter!(<"some_metric_name">, 12, "service" => "http");
/// # }
/// ```
#[proc_macro_hack]
pub use metrics_macros::counter;
/// Updates a gauge.
///
/// Gauges represent a single value that can go up or down over time.
///
/// # Scoped versus unscoped
/// Metrics can be unscoped or scoped, where the scoping is derived by the current module the call
/// is taking place in. This scope is used as a prefix to the provided metric name.
///
/// # Example
/// ```
/// # use metrics::gauge;
/// # fn main() {
/// // A regular, unscoped gauge:
/// gauge!("some_metric_name", 42.2222);
///
/// // A scoped gauge. This inherits a scope derived by the current module:
/// gauge!(<"some_metric_name">, 33.3333);
///
/// // Specifying labels:
/// gauge!("some_metric_name", 66.6666, "service" => "http");
///
/// // And all combined:
/// gauge!("some_metric_name", 55.5555, "service" => "http");
/// gauge!(<"some_metric_name">, 11.1111, "service" => "http");
/// # }
/// ```
#[proc_macro_hack]
pub use metrics_macros::gauge;
/// Records a histogram.
///
/// Histograms measure the distribution of values for a given set of measurements.
///
/// # Scoped versus unscoped
/// Metrics can be unscoped or scoped, where the scoping is derived by the current module the call
/// is taking place in. This scope is used as a prefix to the provided metric name.
///
/// # Implicit conversions
/// Histograms are represented as `u64` values, but often come from another source, such as a time
/// measurement. By default, `histogram!` will accept a `u64` directly or a
/// [`Duration`](std::time::Duration), which uses the nanoseconds total as the converted value.
///
/// External libraries and applications can create their own conversions by implementing the
/// [`IntoU64`] trait for their types, which is required for the value being passed to `histogram!`.
///
/// # Example
/// ```
/// # use metrics::histogram;
/// # use std::time::Duration;
/// # fn main() {
/// // A regular, unscoped histogram:
/// histogram!("some_metric_name", 34);
///
/// // An implicit conversion from `Duration`:
/// let d = Duration::from_millis(17);
/// histogram!("some_metric_name", d);
///
/// // A scoped histogram. This inherits a scope derived by the current module:
/// histogram!(<"some_metric_name">, 38);
/// histogram!(<"some_metric_name">, d);
///
/// // Specifying labels:
/// histogram!("some_metric_name", 38, "service" => "http");
///
/// // And all combined:
/// histogram!("some_metric_name", d, "service" => "http");
/// histogram!(<"some_metric_name">, 57, "service" => "http");
/// # }
/// ```
#[proc_macro_hack]
pub use metrics_macros::histogram;

View File

@ -42,7 +42,10 @@ pub trait Recorder {
fn update_gauge(&self, id: Identifier, value: f64);
/// Records a histogram.
fn record_histogram(&self, id: Identifier, value: f64);
///
/// The value can be value that implements [`IntoU64`]. By default, `metrics` provides an
/// implementation for both `u64` itself as well as [`Duration`](std::time::Duration).
fn record_histogram(&self, id: Identifier, value: u64);
}
struct NoopRecorder;
@ -59,7 +62,7 @@ impl Recorder for NoopRecorder {
}
fn increment_counter(&self, _id: Identifier, _value: u64) {}
fn update_gauge(&self, _id: Identifier, _value: f64) {}
fn record_histogram(&self, _id: Identifier, _value: f64) {}
fn record_histogram(&self, _id: Identifier, _value: u64) {}
}
/// Sets the global recorder to a `&'static Recorder`.