use crate::client::{MetricBackend, StatsdClient};
use crate::types::{Metric, MetricError, MetricResult};
use std::fmt::{self, Write};
use std::marker::PhantomData;
#[derive(PartialEq, Eq, Debug, Hash, Clone, Copy)]
enum MetricValue {
Signed(i64),
Unsigned(u64),
}
impl fmt::Display for MetricValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
MetricValue::Signed(i) => i.fmt(f),
MetricValue::Unsigned(i) => i.fmt(f),
}
}
}
#[derive(PartialEq, Eq, Debug, Hash, Clone, Copy)]
enum MetricType {
Counter,
Timer,
Gauge,
Meter,
Histogram,
Set,
}
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),
}
}
}
#[derive(PartialEq, Eq, Debug, Hash, Clone)]
pub(crate) struct MetricFormatter<'a, T>
where
T: Metric + From<String>,
{
metric: PhantomData<T>,
prefix: &'a str,
key: &'a str,
val: MetricValue,
type_: MetricType,
tags: Option<Vec<(Option<&'a str>, &'a str)>>,
}
impl<'a, T> MetricFormatter<'a, T>
where
T: Metric + From<String>,
{
const TAG_PREFIX: &'static str = "|#";
pub(crate) fn counter(prefix: &'a str, key: &'a str, val: i64) -> Self {
Self::from_i64(prefix, key, val, MetricType::Counter)
}
pub(crate) fn timer(prefix: &'a str, key: &'a str, val: u64) -> Self {
Self::from_u64(prefix, key, val, MetricType::Timer)
}
pub(crate) fn gauge(prefix: &'a str, key: &'a str, val: u64) -> Self {
Self::from_u64(prefix, key, val, MetricType::Gauge)
}
pub(crate) fn meter(prefix: &'a str, key: &'a str, val: u64) -> Self {
Self::from_u64(prefix, key, val, MetricType::Meter)
}
pub(crate) fn histogram(prefix: &'a str, key: &'a str, val: u64) -> Self {
Self::from_u64(prefix, key, val, MetricType::Histogram)
}
pub(crate) fn set(prefix: &'a str, key: &'a str, val: i64) -> Self {
Self::from_i64(prefix, key, val, MetricType::Set)
}
fn from_u64(prefix: &'a str, key: &'a str, val: u64, type_: MetricType) -> Self {
MetricFormatter {
prefix,
key,
type_,
val: MetricValue::Unsigned(val),
metric: PhantomData,
tags: None,
}
}
fn from_i64(prefix: &'a str, key: &'a str, val: i64, type_: MetricType) -> Self {
MetricFormatter {
prefix,
key,
type_,
val: MetricValue::Signed(val),
metric: PhantomData,
tags: None,
}
}
fn with_tag(&mut self, key: &'a str, value: &'a str) {
self.tags
.get_or_insert_with(Vec::new)
.push((Some(key), value));
}
fn with_tag_value(&mut self, value: &'a str) {
self.tags.get_or_insert_with(Vec::new).push((None, value));
}
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 let Some(tags) = self.tags.as_ref() {
out.push_str(Self::TAG_PREFIX);
for (i, &(key, value)) in tags.iter().enumerate() {
if i > 0 {
out.push(',');
}
if let Some(key) = key {
out.push_str(key);
out.push(':');
}
out.push_str(value);
}
}
}
fn base_metric_size_hint(&self) -> usize {
self.prefix.len() + self.key.len() + 1 + 10 + 1 + 2
}
fn tag_size_hint(&self) -> usize {
self.tags.as_ref().map(|t| {
let kv_size: usize = t
.iter()
.map(|tag| tag.0.map_or(0, |k| k.len() + 1 ) + tag.1.len())
.sum();
Self::TAG_PREFIX.len() + kv_size + t.len() - 1
}).unwrap_or(0)
}
pub(crate) fn build(&self) -> T {
let size_hint = self.base_metric_size_hint() + 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);
T::from(metric_string)
}
}
#[derive(Debug)]
enum BuilderRepr<'m, 'c, T>
where
T: Metric + From<String>,
{
Success(MetricFormatter<'m, T>, &'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, T>,
}
impl<'m, 'c, T> MetricBuilder<'m, 'c, T>
where
T: Metric + From<String>,
{
pub(crate) fn new(formatter: MetricFormatter<'m, T>, client: &'c StatsdClient) -> Self {
MetricBuilder {
repr: BuilderRepr::Success(formatter, client),
}
}
pub(crate) fn from_error(err: MetricError, client: &'c StatsdClient) -> Self {
MetricBuilder {
repr: BuilderRepr::Error(err, client),
}
}
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 fn try_send(self) -> MetricResult<T> {
match self.repr {
BuilderRepr::Error(err, _) => Err(err),
BuilderRepr::Success(ref formatter, client) => {
let metric: T = formatter.build();
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::MetricFormatter;
use crate::types::{Counter, Gauge, Histogram, Meter, Metric, Set, Timer};
#[test]
fn test_metric_formatter_counter_no_tags() {
let fmt = MetricFormatter::counter("prefix.", "some.key", 4);
let counter: Counter = fmt.build();
assert_eq!("prefix.some.key:4|c", counter.as_metric_str());
}
#[test]
fn test_metric_formatter_counter_with_tags() {
let mut fmt = MetricFormatter::counter("prefix.", "some.key", 4);
fmt.with_tag("host", "app03.example.com");
fmt.with_tag("bucket", "2");
fmt.with_tag_value("beta");
let counter: Counter = fmt.build();
assert_eq!(
concat!(
"prefix.some.key:4|c|#",
"host:app03.example.com,",
"bucket:2,",
"beta",
),
counter.as_metric_str()
);
}
#[test]
fn test_metric_formatter_timer_no_tags() {
let fmt = MetricFormatter::timer("prefix.", "some.method", 21);
let timer: Timer = fmt.build();
assert_eq!("prefix.some.method:21|ms", timer.as_metric_str());
}
#[test]
fn test_metric_formatter_timer_with_tags() {
let mut fmt = MetricFormatter::timer("prefix.", "some.method", 21);
fmt.with_tag("app", "metrics");
fmt.with_tag_value("async");
let timer: Timer = fmt.build();
assert_eq!(
"prefix.some.method:21|ms|#app:metrics,async",
timer.as_metric_str()
);
}
#[test]
fn test_metric_formatter_gauge_no_tags() {
let fmt = MetricFormatter::gauge("prefix.", "num.failures", 7);
let gauge: Gauge = fmt.build();
assert_eq!("prefix.num.failures:7|g", gauge.as_metric_str());
}
#[test]
fn test_metric_formatter_gauge_with_tags() {
let mut fmt = MetricFormatter::gauge("prefix.", "num.failures", 7);
fmt.with_tag("window", "300");
fmt.with_tag_value("best-effort");
let gauge: Gauge = fmt.build();
assert_eq!(
"prefix.num.failures:7|g|#window:300,best-effort",
gauge.as_metric_str()
);
}
#[test]
fn test_metric_formatter_meter_no_tags() {
let fmt = MetricFormatter::meter("prefix.", "user.logins", 3);
let meter: Meter = fmt.build();
assert_eq!("prefix.user.logins:3|m", meter.as_metric_str());
}
#[test]
fn test_metric_formatter_meter_with_tags() {
let mut fmt = MetricFormatter::meter("prefix.", "user.logins", 3);
fmt.with_tag("user-type", "verified");
fmt.with_tag_value("bucket1");
let meter: Meter = fmt.build();
assert_eq!(
"prefix.user.logins:3|m|#user-type:verified,bucket1",
meter.as_metric_str()
);
}
#[test]
fn test_metric_formatter_histogram_no_tags() {
let fmt = MetricFormatter::histogram("prefix.", "num.results", 44);
let histogram: Histogram = fmt.build();
assert_eq!("prefix.num.results:44|h", histogram.as_metric_str());
}
#[test]
fn test_metric_formatter_histogram_with_tags() {
let mut fmt = MetricFormatter::histogram("prefix.", "num.results", 44);
fmt.with_tag("user-type", "authenticated");
fmt.with_tag_value("source=search");
let histogram: Histogram = fmt.build();
assert_eq!(
concat!(
"prefix.num.results:44|h|#",
"user-type:authenticated,",
"source=search"
),
histogram.as_metric_str()
);
}
#[test]
fn test_metric_formatter_set_no_tags() {
let fmt = MetricFormatter::set("prefix.", "users.uniques", 44);
let set: Set = fmt.build();
assert_eq!("prefix.users.uniques:44|s", set.as_metric_str());
}
#[test]
fn test_metric_formatter_set_with_tags() {
let mut fmt = MetricFormatter::set("prefix.", "users.uniques", 44);
fmt.with_tag("user-type", "authenticated");
fmt.with_tag_value("source=search");
let set: Set = fmt.build();
assert_eq!(
concat!(
"prefix.users.uniques:44|s|#",
"user-type:authenticated,",
"source=search"
),
set.as_metric_str()
);
}
}