diff --git a/CHANGELOG.md b/CHANGELOG.md index eb218cca..fa44cce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.12.0] - unreleased + +### Added + +- Add `Registry::sub_registry_with_label`. See [PR 20]. + +### Changed + +- Rename `Registry::sub_registry` to `Registry::sub_registry_with_prefix`. See + [PR 20]. + +[PR 20]: https://github.com/mxinden/rust-open-metrics-client/pull/20 + ## [0.11.2] - 2021-06-09 ### Fixed - Do not separate labels with spaces. diff --git a/Cargo.toml b/Cargo.toml index 654d6294..d62a160d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "open-metrics-client" -version = "0.11.2" +version = "0.12.0" authors = ["Max Inden "] edition = "2018" description = "Open Metrics client library allowing users to natively instrument applications." diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 4fbefa59..40b9bc5e 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -33,6 +33,7 @@ use crate::metrics::info::Info; use crate::metrics::{MetricType, TypedMetric}; use crate::registry::{Registry, Unit}; +use std::borrow::Cow; use std::collections::HashMap; use std::io::Write; use std::ops::Deref; @@ -77,8 +78,9 @@ where let encoder = Encoder { writer, - name: &desc.name(), + name: desc.name(), unit: desc.unit(), + const_labels: desc.labels(), labels: None, }; @@ -115,15 +117,7 @@ impl Encode for u32 { } } -impl Encode for &str { - fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - // TODO: Can we do better? - writer.write_all(self.as_bytes())?; - Ok(()) - } -} - -impl Encode for Vec { +impl Encode for &[T] { fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { if self.is_empty() { return Ok(()); @@ -142,6 +136,12 @@ impl Encode for Vec { } } +impl Encode for Vec { + fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { + self.as_slice().encode(writer) + } +} + impl Encode for (K, V) { fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { let (key, value) = self; @@ -156,9 +156,23 @@ impl Encode for (K, V) { } } +impl Encode for &str { + fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { + // TODO: Can we do better? + writer.write_all(self.as_bytes())?; + Ok(()) + } +} + impl Encode for String { fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { - writer.write_all(self.as_bytes()).map(|_| ()) + self.as_str().encode(writer) + } +} + +impl<'a> Encode for Cow<'a, str> { + fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> { + self.as_ref().encode(writer) } } @@ -213,6 +227,7 @@ pub struct Encoder<'a, 'b> { writer: &'a mut dyn Write, name: &'a str, unit: &'a Option, + const_labels: &'a [(Cow<'static, str>, Cow<'static, str>)], labels: Option<&'b dyn Encode>, } @@ -245,20 +260,29 @@ impl<'a, 'b> Encoder<'a, 'b> { // TODO: Consider caching the encoded labels for Histograms as they stay the // same but are currently encoded multiple times. fn encode_labels(&mut self) -> Result { - if let Some(labels) = &self.labels { + let mut opened_curly_brackets = false; + + if !self.const_labels.is_empty() { self.writer.write_all(b"{")?; - labels.encode(self.writer)?; + opened_curly_brackets = true; - Ok(BucketEncoder { - opened_curly_brackets: true, - writer: self.writer, - }) - } else { - Ok(BucketEncoder { - opened_curly_brackets: false, - writer: self.writer, - }) + self.const_labels.encode(self.writer)?; } + + if let Some(labels) = &self.labels { + if opened_curly_brackets { + self.writer.write_all(b",")?; + } else { + opened_curly_brackets = true; + self.writer.write_all(b"{")?; + } + labels.encode(self.writer)?; + } + + Ok(BucketEncoder { + opened_curly_brackets, + writer: self.writer, + }) } pub fn with_label_set<'c, 'd>(&'c mut self, label_set: &'d dyn Encode) -> Encoder<'c, 'd> { @@ -268,6 +292,7 @@ impl<'a, 'b> Encoder<'a, 'b> { writer: self.writer, name: self.name, unit: self.unit, + const_labels: self.const_labels, labels: Some(label_set), } } @@ -572,6 +597,7 @@ mod tests { use crate::metrics::gauge::Gauge; use crate::metrics::histogram::exponential_buckets; use pyo3::{prelude::*, types::PyModule}; + use std::borrow::Cow; #[test] fn encode_counter() { @@ -667,6 +693,36 @@ mod tests { parse_with_python_client(String::from_utf8(encoded).unwrap()); } + #[test] + fn encode_counter_family_with_prefix_with_label() { + let mut registry = Registry::default(); + let sub_registry = registry.sub_registry_with_prefix("my_prefix"); + let sub_sub_registry = sub_registry + .sub_registry_with_label((Cow::Borrowed("my_key"), Cow::Borrowed("my_value"))); + let family = Family::, Counter>::default(); + sub_sub_registry.register("my_counter_family", "My counter family", family.clone()); + + family + .get_or_create(&vec![ + ("method".to_string(), "GET".to_string()), + ("status".to_string(), "200".to_string()), + ]) + .inc(); + + let mut encoded = Vec::new(); + + encode(&mut encoded, ®istry).unwrap(); + + let expected = "# HELP my_prefix_my_counter_family My counter family.\n" + .to_owned() + + "# TYPE my_prefix_my_counter_family counter\n" + + "my_prefix_my_counter_family_total{my_key=\"my_value\",method=\"GET\",status=\"200\"} 1\n" + + "# EOF\n"; + assert_eq!(expected, String::from_utf8(encoded.clone()).unwrap()); + + parse_with_python_client(String::from_utf8(encoded).unwrap()); + } + #[test] fn encode_info() { let mut registry = Registry::default(); diff --git a/src/registry.rs b/src/registry.rs index be9fd182..bea07e39 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -2,6 +2,7 @@ //! //! See [`Registry`] for details. +use std::borrow::Cow; use std::ops::Add; /// A metric registry. @@ -59,6 +60,7 @@ use std::ops::Add; /// ``` pub struct Registry> { prefix: Option, + labels: Vec<(Cow<'static, str>, Cow<'static, str>)>, metrics: Vec<(Descriptor, M)>, sub_registries: Vec>, } @@ -67,6 +69,7 @@ impl Default for Registry { fn default() -> Self { Self { prefix: None, + labels: Default::default(), metrics: Default::default(), sub_registries: vec![], } @@ -150,11 +153,13 @@ impl Registry { .unwrap_or(name), help, unit, + labels: self.labels.clone(), }; self.metrics.push((descriptor, metric)); } + // TODO: Update doc. /// Create a sub-registry to register metrics with a common prefix. /// /// Say you would like to prefix one set of metrics with `subsystem_a` and @@ -175,28 +180,53 @@ impl Registry { /// let subsystem_a_counter_1 = Counter::default(); /// let subsystem_a_counter_2 = Counter::default(); /// - /// let subsystem_a_registry = registry.sub_registry("subsystem_a"); + /// let subsystem_a_registry = registry.sub_registry_with_prefix("subsystem_a"); /// registry.register("counter_1", "", subsystem_a_counter_1.clone()); /// registry.register("counter_2", "", subsystem_a_counter_2.clone()); /// /// let subsystem_b_counter_1 = Counter::default(); /// let subsystem_b_counter_2 = Counter::default(); /// - /// let subsystem_a_registry = registry.sub_registry("subsystem_b"); + /// let subsystem_a_registry = registry.sub_registry_with_prefix("subsystem_b"); /// registry.register("counter_1", "", subsystem_b_counter_1.clone()); /// registry.register("counter_2", "", subsystem_b_counter_2.clone()); /// ``` - pub fn sub_registry>(&mut self, prefix: P) -> &mut Self { - let prefix = self - .prefix - .clone() - .map(|p| p + "_") - .unwrap_or_else(|| String::new().into()) - + prefix.as_ref(); + /// + /// See [`Registry::sub_registry_with_label`] for the same functionality, + /// but namespacing with a label instead of a metric name prefix. + pub fn sub_registry_with_prefix>(&mut self, prefix: P) -> &mut Self { + let sub_registry = Registry { + prefix: Some( + self.prefix + .clone() + .map(|p| p + "_") + .unwrap_or_else(|| String::new().into()) + + prefix.as_ref(), + ), + labels: self.labels.clone(), + ..Default::default() + }; + + self.priv_sub_registry(sub_registry) + } + + /// Like [`Registry::sub_registry_with_prefix`] but with a label instead. + pub fn sub_registry_with_label( + &mut self, + label: (Cow<'static, str>, Cow<'static, str>), + ) -> &mut Self { + let mut labels = self.labels.clone(); + labels.push(label); let sub_registry = Registry { - prefix: Some(prefix), + prefix: self.prefix.clone(), + labels, ..Default::default() }; + + self.priv_sub_registry(sub_registry) + } + + fn priv_sub_registry(&mut self, sub_registry: Self) -> &mut Self { self.sub_registries.push(sub_registry); self.sub_registries @@ -280,6 +310,7 @@ pub struct Descriptor { name: String, help: String, unit: Option, + labels: Vec<(Cow<'static, str>, Cow<'static, str>)>, } impl Descriptor { @@ -294,6 +325,10 @@ impl Descriptor { pub fn unit(&self) -> &Option { &self.unit } + + pub fn labels(&self) -> &[(Cow<'static, str>, Cow<'static, str>)] { + &self.labels + } } /// Metric units recommended by Open Metrics. @@ -327,42 +362,74 @@ mod tests { } #[test] - fn sub_registry() { + fn sub_registry_with_prefix_and_label() { let top_level_metric_name = "my_top_level_metric"; let mut registry = Registry::::default(); registry.register(top_level_metric_name, "some help", Default::default()); let prefix_1 = "prefix_1"; let prefix_1_metric_name = "my_prefix_1_metric"; - let sub_registry = registry.sub_registry(prefix_1); + let sub_registry = registry.sub_registry_with_prefix(prefix_1); sub_registry.register(prefix_1_metric_name, "some help", Default::default()); let prefix_1_1 = "prefix_1_1"; let prefix_1_1_metric_name = "my_prefix_1_1_metric"; - let sub_sub_registry = sub_registry.sub_registry(prefix_1_1); + let sub_sub_registry = sub_registry.sub_registry_with_prefix(prefix_1_1); sub_sub_registry.register(prefix_1_1_metric_name, "some help", Default::default()); + let label_1_2 = (Cow::Borrowed("registry"), Cow::Borrowed("1_2")); + let prefix_1_2_metric_name = "my_prefix_1_2_metric"; + let sub_sub_registry = sub_registry.sub_registry_with_label(label_1_2.clone()); + sub_sub_registry.register(prefix_1_2_metric_name, "some help", Default::default()); + + let prefix_1_2_1 = "prefix_1_2_1"; + let prefix_1_2_1_metric_name = "my_prefix_1_2_1_metric"; + let sub_sub_sub_registry = sub_sub_registry.sub_registry_with_prefix(prefix_1_2_1); + sub_sub_sub_registry.register(prefix_1_2_1_metric_name, "some help", Default::default()); + let prefix_2 = "prefix_2"; - let _ = registry.sub_registry(prefix_2); + let _ = registry.sub_registry_with_prefix(prefix_2); let prefix_3 = "prefix_3"; let prefix_3_metric_name = "my_prefix_3_metric"; - let sub_registry = registry.sub_registry(prefix_3); + let sub_registry = registry.sub_registry_with_prefix(prefix_3); sub_registry.register(prefix_3_metric_name, "some help", Default::default()); - let mut metric_iter = registry.iter().map(|(desc, _)| desc.name.clone()); - assert_eq!(Some(top_level_metric_name.to_string()), metric_iter.next()); + let mut metric_iter = registry + .iter() + .map(|(desc, _)| (desc.name.clone(), desc.labels.clone())); + assert_eq!( + Some((top_level_metric_name.to_string(), vec![])), + metric_iter.next() + ); + assert_eq!( + Some((prefix_1.to_string() + "_" + prefix_1_metric_name, vec![])), + metric_iter.next() + ); + assert_eq!( + Some(( + prefix_1.to_string() + "_" + prefix_1_1 + "_" + prefix_1_1_metric_name, + vec![] + )), + metric_iter.next() + ); assert_eq!( - Some(prefix_1.to_string() + "_" + prefix_1_metric_name), + Some(( + prefix_1.to_string() + "_" + prefix_1_2_metric_name, + vec![label_1_2.clone()] + )), metric_iter.next() ); assert_eq!( - Some(prefix_1.to_string() + "_" + prefix_1_1 + "_" + prefix_1_1_metric_name), + Some(( + prefix_1.to_string() + "_" + prefix_1_2_1 + "_" + prefix_1_2_1_metric_name, + vec![label_1_2] + )), metric_iter.next() ); // No metric was registered with prefix 2. assert_eq!( - Some(prefix_3.to_string() + "_" + prefix_3_metric_name), + Some((prefix_3.to_string() + "_" + prefix_3_metric_name, vec![])), metric_iter.next() ); }