wip commit
This commit is contained in:
parent
71ca17754f
commit
35316e5df1
|
@ -5,4 +5,5 @@ members = [
|
|||
"metrics-util",
|
||||
"metrics-benchmark",
|
||||
"metrics-exporter-tcp",
|
||||
"metrics-exporter-prometheus",
|
||||
]
|
||||
|
|
|
@ -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" }
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -24,5 +24,5 @@ message Gauge {
|
|||
}
|
||||
|
||||
message Histogram {
|
||||
double value = 1;
|
||||
uint64 value = 1;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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`.
|
||||
|
|
Loading…
Reference in New Issue