From f01e931ec0a9b7dfbc8160428577d8d35eac847f Mon Sep 17 00:00:00 2001 From: Sebastian Blunt Date: Tue, 24 Dec 2019 13:47:32 -0800 Subject: [PATCH] Add the Tag trait This allows people to write custom implementations of the Tag trait rather than having to rely on format! or similar and passing in tags as str/String. The Tag trait has a blanket implementation for AsRef so this change should be backwards compatible. --- src/lib.rs | 92 ++++++++++++++++++++++++++++++++++++++++++-------- src/metrics.rs | 19 ++++++++--- 2 files changed, 93 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 55ab4248..8cf73675 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,6 +75,8 @@ extern crate chrono; use chrono::Utc; use std::net::UdpSocket; use std::borrow::Cow; +use std::fmt; +use std::io; mod error; pub use self::error::DogstatsdError; @@ -87,6 +89,68 @@ pub use self::metrics::{ServiceStatus, ServiceCheckOptions}; /// A type alias for returning a unit type or an error pub type DogstatsdResult = Result<(), DogstatsdError>; +/// This trait represents anything that can be turned into a tag. +/// +/// There's a blanket implementation of Tag for AsRef, for the most part you +/// can do make do just thinking of this trait as being equivalent to AsRef. +/// +/// What this trait does is allow you to have types that are not AsRef but +/// can still be turned into tags. For example the TagTuple struct lets you turn +/// a tuple of AsRef values into a key-value tag. +pub trait Tag { + /// Write the tag to the given writer + fn write_tag(&self, w: &mut W) -> io::Result<()>; +} + +impl> Tag for T { + fn write_tag(&self, w: &mut W) -> io::Result<()> { + w.write_all(self.as_ref().as_bytes()) + } +} + +/// A newtype around a (K, V) tuple that implements Tag. +/// +/// This will let you do +/// ``` +/// use dogstatsd::{TagTuple, Client, Options}; +/// +/// let client = Client::new(Options::default()).unwrap(); +/// let tags = TagTuple::new("foo", "bar"); +/// client.incr("my_counter", &[tags]); +/// ``` +/// This will add the tag `foo:bar`. +/// +/// Do note that it isn't checked whether the key contains `:`. No character is +/// escaped. If you submit `("ab:cd", "ef")` you'll just end up with `"ab:cd:ef"`. +#[derive(Debug)] +#[repr(transparent)] +pub struct TagTuple + fmt::Debug, V: AsRef + fmt::Debug>((K, V)); +impl + fmt::Debug, V: AsRef + fmt::Debug> Tag for &TagTuple { + fn write_tag(&self, w: &mut W) -> io::Result<()> { + let (key, value) = &self.0; + w.write_all(key.as_ref().as_bytes())?; + w.write_all(b":")?; + w.write_all(value.as_ref().as_bytes())?; + Ok(()) + } +} +impl + fmt::Debug, V: AsRef + fmt::Debug> Tag for TagTuple { + fn write_tag(&self, w: &mut W) -> io::Result<()> { + (&self).write_tag(w) + } +} +impl + fmt::Debug, V: AsRef + fmt::Debug> From<(K, V)> for TagTuple { + fn from(tag: (K, V)) -> Self { + TagTuple(tag) + } +} +impl + fmt::Debug, V: AsRef + fmt::Debug> TagTuple { + /// Create a new tag tuple from a key and a value + pub fn new(k: K, v: V) -> Self { + TagTuple((k, v)) + } +} + /// The struct that represents the options available for the Dogstatsd client. #[derive(Debug, PartialEq)] pub struct Options { @@ -197,7 +261,7 @@ impl Client { pub fn incr<'a, I, S, T>(&self, stat: S, tags: I) -> DogstatsdResult where I: IntoIterator, S: Into>, - T: AsRef, + T: Tag, { self.send(&CountMetric::Incr(stat.into().as_ref()), tags) } @@ -216,7 +280,7 @@ impl Client { pub fn decr<'a, I, S, T>(&self, stat: S, tags: I) -> DogstatsdResult where I: IntoIterator, S: Into>, - T: AsRef, + T: Tag, { self.send(&CountMetric::Decr(stat.into().as_ref()), tags) } @@ -235,7 +299,7 @@ impl Client { pub fn count<'a, I, S, T>(&self, stat: S, count: i64, tags: I) -> DogstatsdResult where I: IntoIterator, S: Into>, - T: AsRef, + T: Tag, { self.send(&CountMetric::Arbitrary(stat.into().as_ref(), count), tags) } @@ -258,7 +322,7 @@ impl Client { where F: FnOnce(), I: IntoIterator, S: Into>, - T: AsRef, + T: Tag, { let start_time = Utc::now(); block(); @@ -280,7 +344,7 @@ impl Client { pub fn timing<'a, I, S, T>(&self, stat: S, ms: i64, tags: I) -> DogstatsdResult where I: IntoIterator, S: Into>, - T: AsRef, + T: Tag, { self.send(&TimingMetric::new(stat.into().as_ref(), ms), tags) } @@ -300,7 +364,7 @@ impl Client { where I: IntoIterator, S: Into>, SS: Into>, - T: AsRef, + T: Tag, { self.send(&GaugeMetric::new(stat.into().as_ref(), val.into().as_ref()), tags) } @@ -320,7 +384,7 @@ impl Client { where I: IntoIterator, S: Into>, SS: Into>, - T: AsRef, + T: Tag, { self.send(&HistogramMetric::new(stat.into().as_ref(), val.into().as_ref()), tags) } @@ -340,7 +404,7 @@ impl Client { where I: IntoIterator, S: Into>, SS: Into>, - T: AsRef, + T: Tag, { self.send(&DistributionMetric::new(stat.into().as_ref(), val.into().as_ref()), tags) } @@ -360,7 +424,7 @@ impl Client { where I: IntoIterator, S: Into>, SS: Into>, - T: AsRef, + T: Tag, { self.send(&SetMetric::new(stat.into().as_ref(), val.into().as_ref()), tags) } @@ -394,7 +458,7 @@ impl Client { pub fn service_check<'a, I, S, T>(&self, stat: S, val: ServiceStatus, tags: I, options: Option) -> DogstatsdResult where I: IntoIterator, S: Into>, - T: AsRef, + T: Tag, { let unwrapped_options = options.unwrap_or_default(); self.send(&ServiceCheck::new(stat.into().as_ref(), val, unwrapped_options), tags) @@ -415,15 +479,15 @@ impl Client { where I: IntoIterator, S: Into>, SS: Into>, - T: AsRef, + T: Tag, { self.send(&Event::new(title.into().as_ref(), text.into().as_ref()), tags) } - fn send(&self, metric: &M, tags: I) -> DogstatsdResult - where I: IntoIterator, + fn send(&self, metric: &M, tags: I) -> DogstatsdResult + where I: IntoIterator, M: Metric, - S: AsRef, + T: Tag, { let formatted_metric = format_for_send(metric, &self.namespace, tags); self.socket.send_to(formatted_metric.as_slice(), &self.to_addr)?; diff --git a/src/metrics.rs b/src/metrics.rs index 268682cc..64138d29 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -1,9 +1,11 @@ +use crate::Tag; + use chrono::{DateTime, Utc}; -pub fn format_for_send(in_metric: &M, in_namespace: &str, tags: I) -> Vec +pub fn format_for_send(in_metric: &M, in_namespace: &str, tags: I) -> Vec where M: Metric, - I: IntoIterator, - S: AsRef, + I: IntoIterator, + T: Tag, { let metric = in_metric.metric_type_format(); let namespace = if in_metric.uses_namespace() { @@ -28,7 +30,7 @@ pub fn format_for_send(in_metric: &M, in_namespace: &str, tags: I) -> V } while next_tag.is_some() { - buf.extend_from_slice(next_tag.unwrap().as_ref().as_bytes()); + next_tag.unwrap().write_tag(&mut buf).unwrap(); next_tag = tags_iter.next(); @@ -418,6 +420,15 @@ mod tests { ) } + #[test] + fn test_format_for_send_tuple_label() { + let labels: crate::TagTuple<_, _> = ("abc", "def").into(); + assert_eq!( + &*b"foo:1|c|#abc:def", + &*format_for_send(&CountMetric::Incr("foo"), "", &[labels]) + ) + } + #[test] fn test_count_incr_metric() { let metric = CountMetric::Incr("incr".into());