use crate::client::{MetricBackend, StatsdClient};
use crate::types::{Metric, MetricError, MetricResult};
use std::fmt::{self, Write};
use std::marker::PhantomData;
#[derive(Debug, Clone, Copy)]
enum MetricType {
Counter,
Timer,
Gauge,
Meter,
Histogram,
Set,
Distribution,
}
impl fmt::Display for MetricType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
MetricType::Counter => "c".fmt(f),
MetricType::Timer => "ms".fmt(f),
MetricType::Gauge => "g".fmt(f),
MetricType::Meter => "m".fmt(f),
MetricType::Histogram => "h".fmt(f),
MetricType::Set => "s".fmt(f),
MetricType::Distribution => "d".fmt(f),
}
}
}
#[derive(Debug, Clone)]
pub enum MetricValue {
Signed(i64),
PackedSigned(Vec<i64>),
Unsigned(u64),
PackedUnsigned(Vec<u64>),
Float(f64),
PackedFloat(Vec<f64>),
}
impl MetricValue {
fn count(&self) -> usize {
match self {
Self::PackedSigned(x) => x.len(),
Self::PackedUnsigned(x) => x.len(),
Self::PackedFloat(x) => x.len(),
_ => 1,
}
}
}
fn write_value<T>(f: &mut fmt::Formatter<'_>, vals: &[T]) -> fmt::Result
where
T: fmt::Display,
{
for (i, value) in vals.iter().enumerate() {
if i > 0 {
f.write_char(':')?;
}
value.fmt(f)?;
}
fmt::Result::Ok(())
}
impl fmt::Display for MetricValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &*self {
MetricValue::Signed(v) => v.fmt(f),
MetricValue::PackedSigned(v) => write_value(f, v),
MetricValue::Unsigned(v) => v.fmt(f),
MetricValue::PackedUnsigned(v) => write_value(f, v),
MetricValue::Float(v) => v.fmt(f),
MetricValue::PackedFloat(v) => write_value(f, v),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct MetricFormatter<'a> {
prefix: &'a str,
key: &'a str,
val: MetricValue,
type_: MetricType,
tags: Vec<(Option<&'a str>, &'a str)>,
base_size: usize,
kv_size: usize,
}
impl<'a> MetricFormatter<'a> {
const TAG_PREFIX: &'static str = "|#";
pub(crate) fn counter(prefix: &'a str, key: &'a str, val: MetricValue) -> Self {
Self::from_val(prefix, key, val, MetricType::Counter)
}
pub(crate) fn timer(prefix: &'a str, key: &'a str, val: MetricValue) -> Self {
Self::from_val(prefix, key, val, MetricType::Timer)
}
pub(crate) fn gauge(prefix: &'a str, key: &'a str, val: MetricValue) -> Self {
Self::from_val(prefix, key, val, MetricType::Gauge)
}
pub(crate) fn meter(prefix: &'a str, key: &'a str, val: MetricValue) -> Self {
Self::from_val(prefix, key, val, MetricType::Meter)
}
pub(crate) fn histogram(prefix: &'a str, key: &'a str, val: MetricValue) -> Self {
Self::from_val(prefix, key, val, MetricType::Histogram)
}
pub(crate) fn distribution(prefix: &'a str, key: &'a str, val: MetricValue) -> Self {
Self::from_val(prefix, key, val, MetricType::Distribution)
}
pub(crate) fn set(prefix: &'a str, key: &'a str, val: MetricValue) -> Self {
Self::from_val(prefix, key, val, MetricType::Set)
}
#[rustfmt::skip]
fn from_val(prefix: &'a str, key: &'a str, val: MetricValue, type_: MetricType) -> Self {
let value_count = val.count();
MetricFormatter {
prefix,
key,
type_,
val,
tags: Vec::new(),
kv_size: 0,
base_size: prefix.len() + key.len() + 1 + 10 * value_count + 1 + 2,
}
}
fn with_tag(&mut self, key: &'a str, value: &'a str) {
self.tags.push((Some(key), value));
self.kv_size += key.len() + 1 + value.len();
}
fn with_tag_value(&mut self, value: &'a str) {
self.tags.push((None, value));
self.kv_size += value.len();
}
fn write_base_metric(&self, out: &mut String) {
let _ = write!(out, "{}{}:{}|{}", self.prefix, self.key, self.val, self.type_);
}
fn write_tags(&self, out: &mut String) {
if !self.tags.is_empty() {
out.push_str(Self::TAG_PREFIX);
for (i, &(key, value)) in self.tags.iter().enumerate() {
if i > 0 {
out.push(',');
}
if let Some(key) = key {
out.push_str(key);
out.push(':');
}
out.push_str(value);
}
}
}
fn tag_size_hint(&self) -> usize {
if self.tags.is_empty() {
return 0;
}
Self::TAG_PREFIX.len() + self.kv_size + self.tags.len() - 1
}
pub(crate) fn format(&self) -> String {
let size_hint = self.base_size + self.tag_size_hint();
let mut metric_string = String::with_capacity(size_hint);
self.write_base_metric(&mut metric_string);
self.write_tags(&mut metric_string);
metric_string
}
}
#[derive(Debug)]
enum BuilderRepr<'m, 'c> {
Success(MetricFormatter<'m>, &'c StatsdClient),
Error(MetricError, &'c StatsdClient),
}
#[must_use = "Did you forget to call .send() after adding tags?"]
#[derive(Debug)]
pub struct MetricBuilder<'m, 'c, T>
where
T: Metric + From<String>,
{
repr: BuilderRepr<'m, 'c>,
type_: PhantomData<T>,
}
impl<'m, 'c, T> MetricBuilder<'m, 'c, T>
where
T: Metric + From<String>,
{
pub(crate) fn from_fmt(formatter: MetricFormatter<'m>, client: &'c StatsdClient) -> Self {
MetricBuilder {
repr: BuilderRepr::Success(formatter, client),
type_: PhantomData,
}
}
pub(crate) fn from_error(err: MetricError, client: &'c StatsdClient) -> Self {
MetricBuilder {
repr: BuilderRepr::Error(err, client),
type_: PhantomData,
}
}
pub fn with_tag(mut self, key: &'m str, value: &'m str) -> Self {
if let BuilderRepr::Success(ref mut formatter, _) = self.repr {
formatter.with_tag(key, value);
}
self
}
pub fn with_tag_value(mut self, value: &'m str) -> Self {
if let BuilderRepr::Success(ref mut formatter, _) = self.repr {
formatter.with_tag_value(value);
}
self
}
pub(crate) fn with_tags<V>(mut self, tags: V) -> Self
where
V: IntoIterator<Item = (Option<&'m str>, &'m str)>,
{
if let BuilderRepr::Success(ref mut formatter, _) = self.repr {
for tag in tags.into_iter() {
match tag {
(Some(key), value) => formatter.with_tag(key, value),
(None, value) => formatter.with_tag_value(value),
}
}
}
self
}
pub fn try_send(self) -> MetricResult<T> {
match self.repr {
BuilderRepr::Error(err, _) => Err(err),
BuilderRepr::Success(ref formatter, client) => {
let metric = T::from(formatter.format());
client.send_metric(&metric)?;
Ok(metric)
}
}
}
pub fn send(self) {
match self.repr {
BuilderRepr::Error(err, client) => client.consume_error(err),
BuilderRepr::Success(_, client) => {
if let Err(e) = self.try_send() {
client.consume_error(e);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::{MetricBuilder, MetricFormatter, MetricValue};
use crate::client::StatsdClient;
use crate::sinks::NopMetricSink;
use crate::test::ErrorMetricSink;
use crate::types::Counter;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
#[test]
fn test_metric_formatter_tag_size_hint_no_tags() {
let fmt = MetricFormatter::counter("prefix.", "some.key", MetricValue::Signed(1));
assert_eq!(0, fmt.tag_size_hint());
}
#[test]
fn test_metric_formatter_tag_size_hint_value() {
let mut fmt = MetricFormatter::counter("prefix.", "some.key", MetricValue::Signed(1));
fmt.with_tag_value("test");
assert_eq!(6, fmt.tag_size_hint());
}
#[test]
fn test_metric_formatter_tag_size_hint_key_value() {
let mut fmt = MetricFormatter::counter("prefix.", "some.key", MetricValue::Signed(1));
fmt.with_tag("host", "web");
fmt.with_tag("user", "123");
assert_eq!(19, fmt.tag_size_hint());
}
#[test]
fn test_metric_formatter_counter_no_tags() {
let fmt = MetricFormatter::counter("prefix.", "some.key", MetricValue::Signed(4));
assert_eq!("prefix.some.key:4|c", &fmt.format());
}
#[test]
fn test_metric_formatter_counter_with_tags() {
let mut fmt = MetricFormatter::counter("prefix.", "some.key", MetricValue::Signed(4));
fmt.with_tag("host", "app03.example.com");
fmt.with_tag("bucket", "2");
fmt.with_tag_value("beta");
assert_eq!(
"prefix.some.key:4|c|#host:app03.example.com,bucket:2,beta",
&fmt.format()
);
}
#[test]
fn test_metric_formatter_timer_no_tags() {
let fmt = MetricFormatter::timer("prefix.", "some.method", MetricValue::Unsigned(21));
assert_eq!("prefix.some.method:21|ms", &fmt.format());
}
#[test]
fn test_metric_formatter_timer_no_tags_multiple_values() {
let fmt = MetricFormatter::timer("prefix.", "some.method", MetricValue::PackedUnsigned(vec![21, 22, 23]));
assert_eq!("prefix.some.method:21:22:23|ms", &fmt.format());
}
#[test]
fn test_metric_formatter_timer_with_tags() {
let mut fmt = MetricFormatter::timer("prefix.", "some.method", MetricValue::Unsigned(21));
fmt.with_tag("app", "metrics");
fmt.with_tag_value("async");
assert_eq!("prefix.some.method:21|ms|#app:metrics,async", &fmt.format());
}
#[test]
fn test_metric_formatter_timer_with_tags_multiple_values() {
let mut fmt = MetricFormatter::timer("prefix.", "some.method", MetricValue::PackedUnsigned(vec![21, 22, 23]));
fmt.with_tag("app", "metrics");
fmt.with_tag_value("async");
assert_eq!("prefix.some.method:21:22:23|ms|#app:metrics,async", &fmt.format());
}
#[test]
fn test_metric_formatter_gauge_no_tags() {
let fmt = MetricFormatter::gauge("prefix.", "num.failures", MetricValue::Unsigned(7));
assert_eq!("prefix.num.failures:7|g", &fmt.format());
}
#[test]
fn test_metric_formatter_gauge_with_tags() {
let mut fmt = MetricFormatter::gauge("prefix.", "num.failures", MetricValue::Unsigned(7));
fmt.with_tag("window", "300");
fmt.with_tag_value("best-effort");
assert_eq!("prefix.num.failures:7|g|#window:300,best-effort", &fmt.format());
}
#[test]
fn test_metric_formatter_meter_no_tags() {
let fmt = MetricFormatter::meter("prefix.", "user.logins", MetricValue::Unsigned(3));
assert_eq!("prefix.user.logins:3|m", &fmt.format());
}
#[test]
fn test_metric_formatter_meter_with_tags() {
let mut fmt = MetricFormatter::meter("prefix.", "user.logins", MetricValue::Unsigned(3));
fmt.with_tag("user-type", "verified");
fmt.with_tag_value("bucket1");
assert_eq!("prefix.user.logins:3|m|#user-type:verified,bucket1", &fmt.format());
}
#[test]
fn test_metric_formatter_histogram_no_tags() {
let fmt = MetricFormatter::histogram("prefix.", "num.results", MetricValue::Unsigned(44));
assert_eq!("prefix.num.results:44|h", &fmt.format());
}
#[test]
fn test_metric_formatter_histogram_no_tags_multiple_values() {
let fmt = MetricFormatter::histogram("prefix.", "num.results", MetricValue::PackedUnsigned(vec![44, 45, 46]));
assert_eq!("prefix.num.results:44:45:46|h", &fmt.format());
}
#[test]
fn test_metric_formatter_histogram_with_tags() {
let mut fmt = MetricFormatter::histogram("prefix.", "num.results", MetricValue::Unsigned(44));
fmt.with_tag("user-type", "authenticated");
fmt.with_tag_value("source=search");
assert_eq!(
"prefix.num.results:44|h|#user-type:authenticated,source=search",
&fmt.format()
);
}
#[test]
fn test_metric_formatter_histogram_with_tags_multiple_values() {
let mut fmt =
MetricFormatter::histogram("prefix.", "num.results", MetricValue::PackedUnsigned(vec![44, 45, 46]));
fmt.with_tag("user-type", "authenticated");
fmt.with_tag_value("source=search");
assert_eq!(
"prefix.num.results:44:45:46|h|#user-type:authenticated,source=search",
&fmt.format()
);
}
#[test]
fn test_metric_formatter_distribution_no_tags() {
let fmt = MetricFormatter::distribution("prefix.", "latency.milliseconds", MetricValue::Unsigned(44));
assert_eq!("prefix.latency.milliseconds:44|d", &fmt.format());
}
#[test]
fn test_metric_formatter_distribution_no_tags_multiple_values() {
let fmt = MetricFormatter::distribution(
"prefix.",
"latency.milliseconds",
MetricValue::PackedUnsigned(vec![44, 45, 46]),
);
assert_eq!("prefix.latency.milliseconds:44:45:46|d", &fmt.format());
}
#[test]
fn test_metric_formatter_distribution_with_tags() {
let mut fmt = MetricFormatter::distribution("prefix.", "latency.milliseconds", MetricValue::Unsigned(44));
fmt.with_tag("user-type", "authenticated");
fmt.with_tag_value("source=search");
assert_eq!(
"prefix.latency.milliseconds:44|d|#user-type:authenticated,source=search",
&fmt.format()
);
}
#[test]
fn test_metric_formatter_distribution_with_tags_multiple_values() {
let mut fmt = MetricFormatter::distribution(
"prefix.",
"latency.milliseconds",
MetricValue::PackedUnsigned(vec![44, 45, 46]),
);
fmt.with_tag("user-type", "authenticated");
fmt.with_tag_value("source=search");
assert_eq!(
"prefix.latency.milliseconds:44:45:46|d|#user-type:authenticated,source=search",
&fmt.format()
);
}
#[test]
fn test_metric_formatter_set_no_tags() {
let fmt = MetricFormatter::set("prefix.", "users.uniques", MetricValue::Signed(44));
assert_eq!("prefix.users.uniques:44|s", &fmt.format());
}
#[test]
fn test_metric_formatter_set_with_tags() {
let mut fmt = MetricFormatter::set("prefix.", "users.uniques", MetricValue::Signed(44));
fmt.with_tag("user-type", "authenticated");
fmt.with_tag_value("source=search");
assert_eq!(
concat!(
"prefix.users.uniques:44|s|#",
"user-type:authenticated,",
"source=search"
),
&fmt.format()
);
}
#[test]
fn test_metric_builder_send_success() {
let fmt = MetricFormatter::counter("prefix.", "some.counter", MetricValue::Signed(11));
let client = StatsdClient::builder("prefix.", NopMetricSink)
.with_error_handler(|e| {
panic!("unexpected error sending metric: {}", e);
})
.build();
let builder: MetricBuilder<'_, '_, Counter> = MetricBuilder::from_fmt(fmt, &client);
builder.send();
}
#[test]
fn test_metric_builder_send_error() {
let errors = Arc::new(AtomicU64::new(0));
let errors_ref = errors.clone();
let fmt = MetricFormatter::counter("prefix.", "some.counter", MetricValue::Signed(11));
let client = StatsdClient::builder("prefix.", ErrorMetricSink::always())
.with_error_handler(move |_e| {
errors_ref.fetch_add(1, Ordering::Release);
})
.build();
let builder: MetricBuilder<'_, '_, Counter> = MetricBuilder::from_fmt(fmt, &client);
builder.send();
assert_eq!(1, errors.load(Ordering::Acquire));
}
#[test]
fn test_metric_builder_try_send_success() {
let fmt = MetricFormatter::counter("prefix.", "some.counter", MetricValue::Signed(11));
let client = StatsdClient::from_sink("prefix.", NopMetricSink);
let builder: MetricBuilder<'_, '_, Counter> = MetricBuilder::from_fmt(fmt, &client);
let res = builder.try_send();
assert!(res.is_ok(), "expected Ok result from try_send");
}
#[test]
fn test_metric_builder_try_send_error() {
let fmt = MetricFormatter::counter("prefix.", "some.counter", MetricValue::Signed(11));
let client = StatsdClient::from_sink("prefix.", ErrorMetricSink::always());
let builder: MetricBuilder<'_, '_, Counter> = MetricBuilder::from_fmt(fmt, &client);
let res = builder.try_send();
assert!(res.is_err(), "expected Err result from try_send");
}
}