From f25f0f4167cae3755fab33cc25b3a46005da9c36 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Tue, 1 Oct 2024 10:41:14 -0300 Subject: [PATCH 01/19] WIP Quote non-legacy metric names in descriptor Signed-off-by: Federico Torres --- Cargo.toml | 1 + src/encoding/text.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5514ab68..dae510be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ parking_lot = "0.12" prometheus-client-derive-encode = { version = "0.4.1", path = "derive-encode" } prost = { version = "0.12.0", optional = true } prost-types = { version = "0.12.0", optional = true } +lazy_static = "1.5.0" [dev-dependencies] async-std = { version = "1", features = ["attributes"] } diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 4946fe35..ef4b1817 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -43,9 +43,23 @@ use crate::metrics::MetricType; use crate::registry::{Prefix, Registry, Unit}; use std::borrow::Cow; +use std::cmp::PartialEq; use std::collections::HashMap; use std::fmt::Write; +use std::sync::Mutex; +use lazy_static::lazy_static; + +#[derive(Debug, PartialEq)] +pub enum ValidationScheme { + LegacyValidation, + UTF8Validation, +} + +lazy_static! { + pub static ref NAME_VALIDATION_SCHEME: Mutex = Mutex::new(ValidationScheme::LegacyValidation); +} + /// Encode both the metrics registered with the provided [`Registry`] and the /// EOF marker into the provided [`Write`]r using the OpenMetrics text format. /// @@ -223,6 +237,9 @@ impl DescriptorEncoder<'_> { metric_type: MetricType, ) -> Result, std::fmt::Error> { self.writer.write_str("# HELP ")?; + if *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_metric_name(name) { + self.writer.write_str("\"")?; + } if let Some(prefix) = self.prefix { self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; @@ -232,11 +249,17 @@ impl DescriptorEncoder<'_> { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; } + if *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_metric_name(name) { + self.writer.write_str("\"")?; + } self.writer.write_str(" ")?; self.writer.write_str(help)?; self.writer.write_str("\n")?; self.writer.write_str("# TYPE ")?; + if *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_metric_name(name) { + self.writer.write_str("\"")?; + } if let Some(prefix) = self.prefix { self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; @@ -246,12 +269,18 @@ impl DescriptorEncoder<'_> { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; } + if *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_metric_name(name) { + self.writer.write_str("\"")?; + } self.writer.write_str(" ")?; self.writer.write_str(metric_type.as_str())?; self.writer.write_str("\n")?; if let Some(unit) = unit { self.writer.write_str("# UNIT ")?; + if *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_metric_name(name) { + self.writer.write_str("\"")?; + } if let Some(prefix) = self.prefix { self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; @@ -259,6 +288,9 @@ impl DescriptorEncoder<'_> { self.writer.write_str(name)?; self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; + if *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_metric_name(name) { + self.writer.write_str("\"")?; + } self.writer.write_str(" ")?; self.writer.write_str(unit.as_str())?; self.writer.write_str("\n")?; @@ -275,6 +307,22 @@ impl DescriptorEncoder<'_> { } } +fn is_valid_legacy_char(c: char, i: usize) -> bool { + c.is_ascii_alphabetic() || c == '_' || c == ':' || (c.is_ascii_digit() && i > 0) +} + +fn is_valid_legacy_metric_name(name: &str) -> bool { + if name.is_empty() { + return false; + } + for (i, c) in name.chars().enumerate() { + if !is_valid_legacy_char(c, i) { + return false; + } + } + true +} + /// Helper type for [`EncodeMetric`](super::EncodeMetric), see /// [`EncodeMetric::encode`](super::EncodeMetric::encode). /// @@ -773,6 +821,26 @@ mod tests { parse_with_python_client(encoded); } + #[test] + fn encode_counter_with_unit_utf8() { + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; + let mut registry = Registry::default(); + let counter: Counter = Counter::default(); + registry.register_with_unit("my.counter", "My counter", Unit::Seconds, counter); + + let mut encoded = String::new(); + encode(&mut encoded, ®istry).unwrap(); + + let expected = "# HELP \"my.counter_seconds\" My counter.\n".to_owned() + + "# TYPE \"my.counter_seconds\" counter\n" + + "# UNIT \"my.counter_seconds\" seconds\n" + + "my.counter_seconds_total 0\n" + + "# EOF\n"; + assert_eq!(expected, encoded); + + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; + } + #[test] fn encode_counter_with_exemplar() { let mut registry = Registry::default(); diff --git a/src/lib.rs b/src/lib.rs index cfff6238..cf8f0822 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ #![deny(dead_code)] -#![deny(missing_docs)] +//#![deny(missing_docs)] #![deny(unused)] #![forbid(unsafe_code)] #![warn(missing_debug_implementations)] From 9d1e5187734ad1397c72871495134c6d9f6949cd Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Fri, 4 Oct 2024 10:37:31 -0300 Subject: [PATCH 02/19] Quote non-legacy label names and put non-legacy quoted metric names inside braces Signed-off-by: Federico Torres --- src/encoding/text.rs | 129 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 106 insertions(+), 23 deletions(-) diff --git a/src/encoding/text.rs b/src/encoding/text.rs index ef4b1817..a2d87d5a 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -237,7 +237,7 @@ impl DescriptorEncoder<'_> { metric_type: MetricType, ) -> Result, std::fmt::Error> { self.writer.write_str("# HELP ")?; - if *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_metric_name(name) { + if is_quoted_metric_name(name, self.prefix) { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { @@ -249,7 +249,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; } - if *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_metric_name(name) { + if is_quoted_metric_name(name, self.prefix) { self.writer.write_str("\"")?; } self.writer.write_str(" ")?; @@ -257,7 +257,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str("\n")?; self.writer.write_str("# TYPE ")?; - if *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_metric_name(name) { + if is_quoted_metric_name(name, self.prefix) { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { @@ -269,7 +269,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; } - if *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_metric_name(name) { + if is_quoted_metric_name(name, self.prefix) { self.writer.write_str("\"")?; } self.writer.write_str(" ")?; @@ -278,7 +278,7 @@ impl DescriptorEncoder<'_> { if let Some(unit) = unit { self.writer.write_str("# UNIT ")?; - if *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_metric_name(name) { + if is_quoted_metric_name(name, self.prefix) { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { @@ -288,7 +288,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str(name)?; self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; - if *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_metric_name(name) { + if is_quoted_metric_name(name, self.prefix) { self.writer.write_str("\"")?; } self.writer.write_str(" ")?; @@ -323,6 +323,33 @@ fn is_valid_legacy_metric_name(name: &str) -> bool { true } +fn is_valid_legacy_prefix(prefix: Option<&Prefix>) -> bool { + match prefix { + Some(prefix) => is_valid_legacy_metric_name(prefix.as_str()), + None => true, + } +} + +fn is_quoted_metric_name(name: &str, prefix: Option<&Prefix>) -> bool { + *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && (!is_valid_legacy_metric_name(name) || !is_valid_legacy_prefix(prefix)) +} + +fn is_valid_legacy_label_name(label_name: &str) -> bool { + if label_name.is_empty() { + return false; + } + for (i, b) in label_name.chars().enumerate() { + if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { + return false; + } + } + true +} + +fn is_quoted_label_name(name: &str) -> bool { + *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_label_name(name) +} + /// Helper type for [`EncodeMetric`](super::EncodeMetric), see /// [`EncodeMetric::encode`](super::EncodeMetric::encode). /// @@ -368,9 +395,9 @@ impl<'a> MetricEncoder<'a> { v: &CounterValue, exemplar: Option<&Exemplar>, ) -> Result<(), std::fmt::Error> { - self.write_prefix_name_unit()?; + self.write_prefix_name_unit_suffix(Option::from("total"))?; - self.write_suffix("total")?; + //self.write_suffix("total")?; self.encode_labels::(None)?; @@ -394,7 +421,7 @@ impl<'a> MetricEncoder<'a> { &mut self, v: &GaugeValue, ) -> Result<(), std::fmt::Error> { - self.write_prefix_name_unit()?; + self.write_prefix_name_unit_suffix(None)?; self.encode_labels::(None)?; @@ -411,9 +438,9 @@ impl<'a> MetricEncoder<'a> { } pub fn encode_info(&mut self, label_set: &S) -> Result<(), std::fmt::Error> { - self.write_prefix_name_unit()?; + self.write_prefix_name_unit_suffix(Option::from("info"))?; - self.write_suffix("info")?; + //self.write_suffix("info")?; self.encode_labels(Some(label_set))?; @@ -450,15 +477,15 @@ impl<'a> MetricEncoder<'a> { buckets: &[(f64, u64)], exemplars: Option<&HashMap>>, ) -> Result<(), std::fmt::Error> { - self.write_prefix_name_unit()?; - self.write_suffix("sum")?; + self.write_prefix_name_unit_suffix(Option::from("sum"))?; + //self.write_suffix("sum")?; self.encode_labels::(None)?; self.writer.write_str(" ")?; self.writer.write_str(dtoa::Buffer::new().format(sum))?; self.newline()?; - self.write_prefix_name_unit()?; - self.write_suffix("count")?; + self.write_prefix_name_unit_suffix(Option::from("count"))?; + //self.write_suffix("count")?; self.encode_labels::(None)?; self.writer.write_str(" ")?; self.writer.write_str(itoa::Buffer::new().format(count))?; @@ -468,8 +495,8 @@ impl<'a> MetricEncoder<'a> { for (i, (upper_bound, count)) in buckets.iter().enumerate() { cummulative += count; - self.write_prefix_name_unit()?; - self.write_suffix("bucket")?; + self.write_prefix_name_unit_suffix(Option::from("bucket"))?; + //self.write_suffix("bucket")?; if *upper_bound == f64::MAX { self.encode_labels(Some(&[("le", "+Inf")]))?; @@ -513,7 +540,11 @@ impl<'a> MetricEncoder<'a> { fn newline(&mut self) -> Result<(), std::fmt::Error> { self.writer.write_str("\n") } - fn write_prefix_name_unit(&mut self) -> Result<(), std::fmt::Error> { + fn write_prefix_name_unit_suffix(&mut self, suffix: Option<&'static str>) -> Result<(), std::fmt::Error> { + if is_quoted_metric_name(self.name, self.prefix) { + self.writer.write_str("{")?; + self.writer.write_str("\"")?; + } if let Some(prefix) = self.prefix { self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; @@ -523,16 +554,23 @@ impl<'a> MetricEncoder<'a> { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; } + if let Some(suffix) = suffix { + self.writer.write_str("_")?; + self.writer.write_str(suffix)?; + } + if is_quoted_metric_name(self.name, self.prefix) { + self.writer.write_str("\"")?; + } Ok(()) } - fn write_suffix(&mut self, suffix: &'static str) -> Result<(), std::fmt::Error> { +/* fn write_suffix(&mut self, suffix: &'static str) -> Result<(), std::fmt::Error> { self.writer.write_str("_")?; self.writer.write_str(suffix)?; Ok(()) - } + }*/ // TODO: Consider caching the encoded labels for Histograms as they stay the // same but are currently encoded multiple times. @@ -544,10 +582,17 @@ impl<'a> MetricEncoder<'a> { && additional_labels.is_none() && self.family_labels.is_none() { + if is_quoted_metric_name(self.name, self.prefix) { + self.writer.write_str("}")?; + } return Ok(()); } - self.writer.write_str("{")?; + if is_quoted_metric_name(self.name, self.prefix) { + self.writer.write_str(", ")?; + } else { + self.writer.write_str("{")?; + } self.const_labels .encode(LabelSetEncoder::new(self.writer).into())?; @@ -745,7 +790,14 @@ impl<'a> LabelKeyEncoder<'a> { impl<'a> std::fmt::Write for LabelKeyEncoder<'a> { fn write_str(&mut self, s: &str) -> std::fmt::Result { - self.writer.write_str(s) + if is_quoted_label_name(s) { + self.writer.write_str("\"")?; + } + self.writer.write_str(s)?; + if is_quoted_label_name(s) { + self.writer.write_str("\"")?; + } + Ok(()) } } @@ -834,7 +886,7 @@ mod tests { let expected = "# HELP \"my.counter_seconds\" My counter.\n".to_owned() + "# TYPE \"my.counter_seconds\" counter\n" + "# UNIT \"my.counter_seconds\" seconds\n" - + "my.counter_seconds_total 0\n" + + "{\"my.counter_seconds_total\"} 0\n" + "# EOF\n"; assert_eq!(expected, encoded); @@ -941,6 +993,37 @@ mod tests { parse_with_python_client(encoded); } + #[test] + fn encode_counter_family_with_prefix_with_label_utf8() { + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; + 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 = String::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, encoded); + + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; + } + #[test] fn encode_info() { let mut registry = Registry::default(); From 378d0e413ddf334c63a28555097935e91d057f71 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Fri, 4 Oct 2024 16:10:53 -0300 Subject: [PATCH 03/19] Add quoted metric and label names tests for text encoding Signed-off-by: Federico Torres --- src/encoding/text.rs | 224 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 220 insertions(+), 4 deletions(-) diff --git a/src/encoding/text.rs b/src/encoding/text.rs index a2d87d5a..bc8ac736 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -589,7 +589,7 @@ impl<'a> MetricEncoder<'a> { } if is_quoted_metric_name(self.name, self.prefix) { - self.writer.write_str(", ")?; + self.writer.write_str(",")?; } else { self.writer.write_str("{")?; } @@ -874,7 +874,7 @@ mod tests { } #[test] - fn encode_counter_with_unit_utf8() { + fn encode_counter_with_unit_and_quoted_metric_name() { *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; let mut registry = Registry::default(); let counter: Counter = Counter::default(); @@ -922,6 +922,36 @@ mod tests { parse_with_python_client(encoded); } + #[test] + fn encode_counter_with_exemplar_and_quoted_metric_name() { + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; + let mut registry = Registry::default(); + + let counter_with_exemplar: CounterWithExemplar> = + CounterWithExemplar::default(); + registry.register_with_unit( + "my.counter.with.exemplar", + "My counter with exemplar", + Unit::Seconds, + counter_with_exemplar.clone(), + ); + + counter_with_exemplar.inc_by(1, Some(vec![("user_id".to_string(), 42)])); + + let mut encoded = String::new(); + encode(&mut encoded, ®istry).unwrap(); + + let expected = "# HELP \"my.counter.with.exemplar_seconds\" My counter with exemplar.\n" + .to_owned() + + "# TYPE \"my.counter.with.exemplar_seconds\" counter\n" + + "# UNIT \"my.counter.with.exemplar_seconds\" seconds\n" + + "{\"my.counter.with.exemplar_seconds_total\"} 1 # {user_id=\"42\"} 1.0\n" + + "# EOF\n"; + assert_eq!(expected, encoded); + + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; + } + #[test] fn encode_gauge() { let mut registry = Registry::default(); @@ -994,7 +1024,7 @@ mod tests { } #[test] - fn encode_counter_family_with_prefix_with_label_utf8() { + fn encode_counter_family_with_prefix_with_label_with_quoted_metric_and_label_names() { *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; let mut registry = Registry::default(); let sub_registry = registry.sub_registry_with_prefix("my.prefix"); @@ -1017,7 +1047,7 @@ mod tests { 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" + + "{\"my.prefix_my_counter_family_total\",\"my.key\"=\"my_value\",method=\"GET\",status=\"200\"} 1\n" + "# EOF\n"; assert_eq!(expected, encoded); @@ -1042,6 +1072,25 @@ mod tests { parse_with_python_client(encoded); } + #[test] + fn encode_info_with_quoted_metric_and_label_names() { + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; + let mut registry = Registry::default(); + let info = Info::new(vec![("os.foo".to_string(), "GNU/linux".to_string())]); + registry.register("my.info.metric", "My info metric", info); + + let mut encoded = String::new(); + encode(&mut encoded, ®istry).unwrap(); + + let expected = "# HELP \"my.info.metric\" My info metric.\n".to_owned() + + "# TYPE \"my.info.metric\" info\n" + + "{\"my.info.metric_info\",\"os.foo\"=\"GNU/linux\"} 1\n" + + "# EOF\n"; + assert_eq!(expected, encoded); + + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; + } + #[test] fn encode_histogram() { let mut registry = Registry::default(); @@ -1132,6 +1181,38 @@ mod tests { parse_with_python_client(encoded); } + #[test] + fn encode_histogram_with_exemplars_and_quoted_metric_name() { + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; + let mut registry = Registry::default(); + let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10)); + registry.register("my.histogram", "My histogram", histogram.clone()); + histogram.observe(1.0, Some([("user_id".to_string(), 42u64)])); + + let mut encoded = String::new(); + encode(&mut encoded, ®istry).unwrap(); + + let expected = "# HELP \"my.histogram\" My histogram.\n".to_owned() + + "# TYPE \"my.histogram\" histogram\n" + + "{\"my.histogram_sum\"} 1.0\n" + + "{\"my.histogram_count\"} 1\n" + + "{\"my.histogram_bucket\",le=\"1.0\"} 1 # {user_id=\"42\"} 1.0\n" + + "{\"my.histogram_bucket\",le=\"2.0\"} 1\n" + + "{\"my.histogram_bucket\",le=\"4.0\"} 1\n" + + "{\"my.histogram_bucket\",le=\"8.0\"} 1\n" + + "{\"my.histogram_bucket\",le=\"16.0\"} 1\n" + + "{\"my.histogram_bucket\",le=\"32.0\"} 1\n" + + "{\"my.histogram_bucket\",le=\"64.0\"} 1\n" + + "{\"my.histogram_bucket\",le=\"128.0\"} 1\n" + + "{\"my.histogram_bucket\",le=\"256.0\"} 1\n" + + "{\"my.histogram_bucket\",le=\"512.0\"} 1\n" + + "{\"my.histogram_bucket\",le=\"+Inf\"} 1\n" + + "# EOF\n"; + assert_eq!(expected, encoded); + + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; + } + #[test] fn sub_registry_with_prefix_and_label() { let top_level_metric_name = "my_top_level_metric"; @@ -1206,6 +1287,81 @@ mod tests { parse_with_python_client(encoded); } + #[test] + fn sub_registry_with_prefix_and_label_and_quoted_metric_and_label_names() { + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; + let top_level_metric_name = "my.top.level.metric"; + let mut registry = Registry::default(); + let counter: Counter = Counter::default(); + registry.register(top_level_metric_name, "some help", counter.clone()); + + let prefix_1 = "prefix.1"; + let prefix_1_metric_name = "my_prefix_1_metric"; + let sub_registry = registry.sub_registry_with_prefix(prefix_1); + sub_registry.register(prefix_1_metric_name, "some help", counter.clone()); + + 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_with_prefix(prefix_1_1); + sub_sub_registry.register(prefix_1_1_metric_name, "some help", counter.clone()); + + let label_1_2 = (Cow::Borrowed("registry.foo"), 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", counter.clone()); + + let labels_1_3 = vec![ + (Cow::Borrowed("label_1_3_1"), Cow::Borrowed("value_1_3_1")), + (Cow::Borrowed("label_1_3_2"), Cow::Borrowed("value_1_3_2")), + ]; + let prefix_1_3_metric_name = "my_prefix_1_3_metric"; + let sub_sub_registry = + sub_registry.sub_registry_with_labels(labels_1_3.clone().into_iter()); + sub_sub_registry.register(prefix_1_3_metric_name, "some help", counter.clone()); + + let prefix_1_3_1 = "prefix_1_3_1"; + let prefix_1_3_1_metric_name = "my_prefix_1_3_1_metric"; + let sub_sub_sub_registry = sub_sub_registry.sub_registry_with_prefix(prefix_1_3_1); + sub_sub_sub_registry.register(prefix_1_3_1_metric_name, "some help", counter.clone()); + + let prefix_2 = "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_with_prefix(prefix_3); + sub_registry.register(prefix_3_metric_name, "some help", counter); + + let mut encoded = String::new(); + encode(&mut encoded, ®istry).unwrap(); + + let expected = "# HELP \"my.top.level.metric\" some help.\n".to_owned() + + "# TYPE \"my.top.level.metric\" counter\n" + + "{\"my.top.level.metric_total\"} 0\n" + + "# HELP \"prefix.1_my_prefix_1_metric\" some help.\n" + + "# TYPE \"prefix.1_my_prefix_1_metric\" counter\n" + + "{\"prefix.1_my_prefix_1_metric_total\"} 0\n" + + "# HELP \"prefix.1_prefix_1_1_my_prefix_1_1_metric\" some help.\n" + + "# TYPE \"prefix.1_prefix_1_1_my_prefix_1_1_metric\" counter\n" + + "{\"prefix.1_prefix_1_1_my_prefix_1_1_metric_total\"} 0\n" + + "# HELP \"prefix.1_my_prefix_1_2_metric\" some help.\n" + + "# TYPE \"prefix.1_my_prefix_1_2_metric\" counter\n" + + "{\"prefix.1_my_prefix_1_2_metric_total\",\"registry.foo\"=\"1_2\"} 0\n" + + "# HELP \"prefix.1_my_prefix_1_3_metric\" some help.\n" + + "# TYPE \"prefix.1_my_prefix_1_3_metric\" counter\n" + + "{\"prefix.1_my_prefix_1_3_metric_total\",label_1_3_1=\"value_1_3_1\",label_1_3_2=\"value_1_3_2\"} 0\n" + + "# HELP \"prefix.1_prefix_1_3_1_my_prefix_1_3_1_metric\" some help.\n" + + "# TYPE \"prefix.1_prefix_1_3_1_my_prefix_1_3_1_metric\" counter\n" + + "{\"prefix.1_prefix_1_3_1_my_prefix_1_3_1_metric_total\",label_1_3_1=\"value_1_3_1\",label_1_3_2=\"value_1_3_2\"} 0\n" + + "# HELP prefix_3_my_prefix_3_metric some help.\n" + + "# TYPE prefix_3_my_prefix_3_metric counter\n" + + "prefix_3_my_prefix_3_metric_total 0\n" + + "# EOF\n"; + assert_eq!(expected, encoded); + + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; + } + #[test] fn sub_registry_collector() { use crate::encoding::EncodeMetric; @@ -1265,6 +1421,66 @@ mod tests { parse_with_python_client(encoded); } + #[test] + fn sub_registry_collector_with_quoted_metric_names() { + use crate::encoding::EncodeMetric; + + #[derive(Debug)] + struct Collector { + name: String, + } + + impl Collector { + fn new(name: impl Into) -> Self { + Self { name: name.into() } + } + } + + impl crate::collector::Collector for Collector { + fn encode( + &self, + mut encoder: crate::encoding::DescriptorEncoder, + ) -> Result<(), std::fmt::Error> { + let counter = crate::metrics::counter::ConstCounter::new(42u64); + let metric_encoder = encoder.encode_descriptor( + &self.name, + "some help", + None, + counter.metric_type(), + )?; + counter.encode(metric_encoder)?; + Ok(()) + } + } + + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; + let mut registry = Registry::default(); + registry.register_collector(Box::new(Collector::new("top.level"))); + + let sub_registry = registry.sub_registry_with_prefix("prefix.1"); + sub_registry.register_collector(Box::new(Collector::new("sub_level"))); + + let sub_sub_registry = sub_registry.sub_registry_with_prefix("prefix_1_2"); + sub_sub_registry.register_collector(Box::new(Collector::new("sub_sub_level"))); + + let mut encoded = String::new(); + encode(&mut encoded, ®istry).unwrap(); + + let expected = "# HELP \"top.level\" some help\n".to_owned() + + "# TYPE \"top.level\" counter\n" + + "{\"top.level_total\"} 42\n" + + "# HELP \"prefix.1_sub_level\" some help\n" + + "# TYPE \"prefix.1_sub_level\" counter\n" + + "{\"prefix.1_sub_level_total\"} 42\n" + + "# HELP \"prefix.1_prefix_1_2_sub_sub_level\" some help\n" + + "# TYPE \"prefix.1_prefix_1_2_sub_sub_level\" counter\n" + + "{\"prefix.1_prefix_1_2_sub_sub_level_total\"} 42\n" + + "# EOF\n"; + assert_eq!(expected, encoded); + + *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; + } + #[test] fn encode_registry_eof() { let mut orders_registry = Registry::default(); From b7f6396731211978be712cebfde428c3c1b1bc13 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Thu, 10 Oct 2024 16:46:59 -0300 Subject: [PATCH 04/19] Refactor metric and label names validation Signed-off-by: Federico Torres --- Cargo.toml | 1 - src/encoding/text.rs | 110 ++++++++++++++++++++----------------------- src/registry.rs | 12 +++++ 3 files changed, 63 insertions(+), 60 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dae510be..5514ab68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ parking_lot = "0.12" prometheus-client-derive-encode = { version = "0.4.1", path = "derive-encode" } prost = { version = "0.12.0", optional = true } prost-types = { version = "0.12.0", optional = true } -lazy_static = "1.5.0" [dev-dependencies] async-std = { version = "1", features = ["attributes"] } diff --git a/src/encoding/text.rs b/src/encoding/text.rs index bc8ac736..5ac95612 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -47,19 +47,13 @@ use std::cmp::PartialEq; use std::collections::HashMap; use std::fmt::Write; -use std::sync::Mutex; -use lazy_static::lazy_static; - -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Default, Clone)] pub enum ValidationScheme { + #[default] LegacyValidation, UTF8Validation, } -lazy_static! { - pub static ref NAME_VALIDATION_SCHEME: Mutex = Mutex::new(ValidationScheme::LegacyValidation); -} - /// Encode both the metrics registered with the provided [`Registry`] and the /// EOF marker into the provided [`Write`]r using the OpenMetrics text format. /// @@ -156,7 +150,7 @@ pub fn encode_registry(writer: &mut W, registry: &Registry) -> Result<(), std where W: Write, { - registry.encode(&mut DescriptorEncoder::new(writer).into()) + registry.encode(&mut DescriptorEncoder::new(writer, ®istry.name_validation_scheme).into()) } /// Encode the EOF marker into the provided [`Write`]r using the OpenMetrics @@ -200,6 +194,7 @@ pub(crate) struct DescriptorEncoder<'a> { writer: &'a mut dyn Write, prefix: Option<&'a Prefix>, labels: &'a [(Cow<'static, str>, Cow<'static, str>)], + name_validation_scheme: &'a ValidationScheme, } impl<'a> std::fmt::Debug for DescriptorEncoder<'a> { @@ -209,11 +204,12 @@ impl<'a> std::fmt::Debug for DescriptorEncoder<'a> { } impl DescriptorEncoder<'_> { - pub(crate) fn new(writer: &mut dyn Write) -> DescriptorEncoder { + pub(crate) fn new<'a>(writer: &'a mut dyn Write, name_validation_scheme: &'a ValidationScheme) -> DescriptorEncoder<'a> { DescriptorEncoder { writer, prefix: Default::default(), labels: Default::default(), + name_validation_scheme, } } @@ -226,6 +222,7 @@ impl DescriptorEncoder<'_> { prefix, labels, writer: self.writer, + name_validation_scheme: &self.name_validation_scheme, } } @@ -237,7 +234,7 @@ impl DescriptorEncoder<'_> { metric_type: MetricType, ) -> Result, std::fmt::Error> { self.writer.write_str("# HELP ")?; - if is_quoted_metric_name(name, self.prefix) { + if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { @@ -249,7 +246,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; } - if is_quoted_metric_name(name, self.prefix) { + if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { self.writer.write_str("\"")?; } self.writer.write_str(" ")?; @@ -257,7 +254,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str("\n")?; self.writer.write_str("# TYPE ")?; - if is_quoted_metric_name(name, self.prefix) { + if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { @@ -269,7 +266,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; } - if is_quoted_metric_name(name, self.prefix) { + if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { self.writer.write_str("\"")?; } self.writer.write_str(" ")?; @@ -278,7 +275,7 @@ impl DescriptorEncoder<'_> { if let Some(unit) = unit { self.writer.write_str("# UNIT ")?; - if is_quoted_metric_name(name, self.prefix) { + if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { @@ -288,7 +285,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str(name)?; self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; - if is_quoted_metric_name(name, self.prefix) { + if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { self.writer.write_str("\"")?; } self.writer.write_str(" ")?; @@ -303,6 +300,7 @@ impl DescriptorEncoder<'_> { unit, const_labels: self.labels, family_labels: None, + name_validation_scheme: self.name_validation_scheme, }) } } @@ -330,10 +328,17 @@ fn is_valid_legacy_prefix(prefix: Option<&Prefix>) -> bool { } } +/* fn is_quoted_metric_name(name: &str, prefix: Option<&Prefix>) -> bool { *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && (!is_valid_legacy_metric_name(name) || !is_valid_legacy_prefix(prefix)) } + */ + +fn is_quoted_metric_name(name: &str, prefix: Option<&Prefix>, validation_scheme: &ValidationScheme) -> bool { + *validation_scheme == ValidationScheme::UTF8Validation && (!is_valid_legacy_metric_name(name) || !is_valid_legacy_prefix(prefix)) +} + fn is_valid_legacy_label_name(label_name: &str) -> bool { if label_name.is_empty() { return false; @@ -346,8 +351,8 @@ fn is_valid_legacy_label_name(label_name: &str) -> bool { true } -fn is_quoted_label_name(name: &str) -> bool { - *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && !is_valid_legacy_label_name(name) +fn is_quoted_label_name(name: &str, validation_scheme: &ValidationScheme) -> bool { + *validation_scheme == ValidationScheme::UTF8Validation && !is_valid_legacy_label_name(name) } /// Helper type for [`EncodeMetric`](super::EncodeMetric), see @@ -366,13 +371,14 @@ pub(crate) struct MetricEncoder<'a> { unit: Option<&'a Unit>, const_labels: &'a [(Cow<'static, str>, Cow<'static, str>)], family_labels: Option<&'a dyn super::EncodeLabelSet>, + name_validation_scheme: &'a ValidationScheme, } impl<'a> std::fmt::Debug for MetricEncoder<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut labels = String::new(); if let Some(l) = self.family_labels { - l.encode(LabelSetEncoder::new(&mut labels).into())?; + l.encode(LabelSetEncoder::new(&mut labels, self.name_validation_scheme).into())?; } f.debug_struct("Encoder") @@ -467,6 +473,7 @@ impl<'a> MetricEncoder<'a> { unit: self.unit, const_labels: self.const_labels, family_labels: Some(label_set), + name_validation_scheme: self.name_validation_scheme, }) } @@ -526,7 +533,7 @@ impl<'a> MetricEncoder<'a> { self.writer.write_str(" # {")?; exemplar .label_set - .encode(LabelSetEncoder::new(self.writer).into())?; + .encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme).into())?; self.writer.write_str("} ")?; exemplar.value.encode( ExemplarValueEncoder { @@ -541,7 +548,7 @@ impl<'a> MetricEncoder<'a> { self.writer.write_str("\n") } fn write_prefix_name_unit_suffix(&mut self, suffix: Option<&'static str>) -> Result<(), std::fmt::Error> { - if is_quoted_metric_name(self.name, self.prefix) { + if is_quoted_metric_name(self.name, self.prefix, &self.name_validation_scheme) { self.writer.write_str("{")?; self.writer.write_str("\"")?; } @@ -558,7 +565,7 @@ impl<'a> MetricEncoder<'a> { self.writer.write_str("_")?; self.writer.write_str(suffix)?; } - if is_quoted_metric_name(self.name, self.prefix) { + if is_quoted_metric_name(self.name, self.prefix, &self.name_validation_scheme) { self.writer.write_str("\"")?; } @@ -582,27 +589,27 @@ impl<'a> MetricEncoder<'a> { && additional_labels.is_none() && self.family_labels.is_none() { - if is_quoted_metric_name(self.name, self.prefix) { + if is_quoted_metric_name(self.name, self.prefix, &self.name_validation_scheme) { self.writer.write_str("}")?; } return Ok(()); } - if is_quoted_metric_name(self.name, self.prefix) { + if is_quoted_metric_name(self.name, self.prefix, &self.name_validation_scheme) { self.writer.write_str(",")?; } else { self.writer.write_str("{")?; } self.const_labels - .encode(LabelSetEncoder::new(self.writer).into())?; + .encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme).into())?; if let Some(additional_labels) = additional_labels { if !self.const_labels.is_empty() { self.writer.write_str(",")?; } - additional_labels.encode(LabelSetEncoder::new(self.writer).into())?; + additional_labels.encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme).into())?; } /// Writer impl which prepends a comma on the first call to write output to the wrapped writer @@ -632,9 +639,9 @@ impl<'a> MetricEncoder<'a> { writer: self.writer, should_prepend: true, }; - labels.encode(LabelSetEncoder::new(&mut writer).into())?; + labels.encode(LabelSetEncoder::new(&mut writer, self.name_validation_scheme).into())?; } else { - labels.encode(LabelSetEncoder::new(self.writer).into())?; + labels.encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme).into())?; }; } @@ -717,6 +724,7 @@ impl<'a> ExemplarValueEncoder<'a> { pub(crate) struct LabelSetEncoder<'a> { writer: &'a mut dyn Write, first: bool, + name_validation_scheme: &'a ValidationScheme, } impl<'a> std::fmt::Debug for LabelSetEncoder<'a> { @@ -728,10 +736,11 @@ impl<'a> std::fmt::Debug for LabelSetEncoder<'a> { } impl<'a> LabelSetEncoder<'a> { - fn new(writer: &'a mut dyn Write) -> Self { + fn new(writer: &'a mut dyn Write, name_validation_scheme: &'a ValidationScheme) -> Self { Self { writer, first: true, + name_validation_scheme, } } @@ -741,6 +750,7 @@ impl<'a> LabelSetEncoder<'a> { LabelEncoder { writer: self.writer, first, + name_validation_scheme: self.name_validation_scheme, } } } @@ -748,6 +758,7 @@ impl<'a> LabelSetEncoder<'a> { pub(crate) struct LabelEncoder<'a> { writer: &'a mut dyn Write, first: bool, + name_validation_scheme: &'a ValidationScheme, } impl<'a> std::fmt::Debug for LabelEncoder<'a> { @@ -765,12 +776,14 @@ impl<'a> LabelEncoder<'a> { } Ok(LabelKeyEncoder { writer: self.writer, + name_validation_scheme: self.name_validation_scheme, }) } } pub(crate) struct LabelKeyEncoder<'a> { writer: &'a mut dyn Write, + name_validation_scheme: &'a ValidationScheme, } impl<'a> std::fmt::Debug for LabelKeyEncoder<'a> { @@ -790,11 +803,11 @@ impl<'a> LabelKeyEncoder<'a> { impl<'a> std::fmt::Write for LabelKeyEncoder<'a> { fn write_str(&mut self, s: &str) -> std::fmt::Result { - if is_quoted_label_name(s) { + if is_quoted_label_name(s, self.name_validation_scheme) { self.writer.write_str("\"")?; } self.writer.write_str(s)?; - if is_quoted_label_name(s) { + if is_quoted_label_name(s, self.name_validation_scheme) { self.writer.write_str("\"")?; } Ok(()) @@ -875,8 +888,7 @@ mod tests { #[test] fn encode_counter_with_unit_and_quoted_metric_name() { - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; - let mut registry = Registry::default(); + let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); let counter: Counter = Counter::default(); registry.register_with_unit("my.counter", "My counter", Unit::Seconds, counter); @@ -889,8 +901,6 @@ mod tests { + "{\"my.counter_seconds_total\"} 0\n" + "# EOF\n"; assert_eq!(expected, encoded); - - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; } #[test] @@ -924,8 +934,7 @@ mod tests { #[test] fn encode_counter_with_exemplar_and_quoted_metric_name() { - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; - let mut registry = Registry::default(); + let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); let counter_with_exemplar: CounterWithExemplar> = CounterWithExemplar::default(); @@ -948,8 +957,6 @@ mod tests { + "{\"my.counter.with.exemplar_seconds_total\"} 1 # {user_id=\"42\"} 1.0\n" + "# EOF\n"; assert_eq!(expected, encoded); - - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; } #[test] @@ -1025,8 +1032,7 @@ mod tests { #[test] fn encode_counter_family_with_prefix_with_label_with_quoted_metric_and_label_names() { - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; - let mut registry = Registry::default(); + let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); 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"))); @@ -1050,8 +1056,6 @@ mod tests { + "{\"my.prefix_my_counter_family_total\",\"my.key\"=\"my_value\",method=\"GET\",status=\"200\"} 1\n" + "# EOF\n"; assert_eq!(expected, encoded); - - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; } #[test] @@ -1074,8 +1078,7 @@ mod tests { #[test] fn encode_info_with_quoted_metric_and_label_names() { - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; - let mut registry = Registry::default(); + let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); let info = Info::new(vec![("os.foo".to_string(), "GNU/linux".to_string())]); registry.register("my.info.metric", "My info metric", info); @@ -1087,8 +1090,6 @@ mod tests { + "{\"my.info.metric_info\",\"os.foo\"=\"GNU/linux\"} 1\n" + "# EOF\n"; assert_eq!(expected, encoded); - - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; } #[test] @@ -1183,8 +1184,7 @@ mod tests { #[test] fn encode_histogram_with_exemplars_and_quoted_metric_name() { - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; - let mut registry = Registry::default(); + let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10)); registry.register("my.histogram", "My histogram", histogram.clone()); histogram.observe(1.0, Some([("user_id".to_string(), 42u64)])); @@ -1209,8 +1209,6 @@ mod tests { + "{\"my.histogram_bucket\",le=\"+Inf\"} 1\n" + "# EOF\n"; assert_eq!(expected, encoded); - - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; } #[test] @@ -1289,9 +1287,8 @@ mod tests { #[test] fn sub_registry_with_prefix_and_label_and_quoted_metric_and_label_names() { - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; let top_level_metric_name = "my.top.level.metric"; - let mut registry = Registry::default(); + let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); let counter: Counter = Counter::default(); registry.register(top_level_metric_name, "some help", counter.clone()); @@ -1358,8 +1355,6 @@ mod tests { + "prefix_3_my_prefix_3_metric_total 0\n" + "# EOF\n"; assert_eq!(expected, encoded); - - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; } #[test] @@ -1453,8 +1448,7 @@ mod tests { } } - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::UTF8Validation; - let mut registry = Registry::default(); + let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); registry.register_collector(Box::new(Collector::new("top.level"))); let sub_registry = registry.sub_registry_with_prefix("prefix.1"); @@ -1477,8 +1471,6 @@ mod tests { + "{\"prefix.1_prefix_1_2_sub_sub_level_total\"} 42\n" + "# EOF\n"; assert_eq!(expected, encoded); - - *NAME_VALIDATION_SCHEME.lock().unwrap() = ValidationScheme::LegacyValidation; } #[test] diff --git a/src/registry.rs b/src/registry.rs index c7dec3ba..d787b97a 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -6,6 +6,7 @@ use std::borrow::Cow; use crate::collector::Collector; use crate::encoding::{DescriptorEncoder, EncodeMetric}; +use crate::encoding::text::ValidationScheme; /// A metric registry. /// @@ -64,6 +65,7 @@ pub struct Registry { metrics: Vec<(Descriptor, Box)>, collectors: Vec>, sub_registries: Vec, + pub name_validation_scheme: ValidationScheme, } impl Registry { @@ -96,6 +98,16 @@ impl Registry { ..Default::default() } } + + pub fn with_name_validation_scheme( + name_validation_scheme: ValidationScheme + ) -> Self { + Self { + name_validation_scheme, + ..Default::default() + } + } + /// Register a metric with the [`Registry`]. /// From 23397697ab275da96515b894a0c942322bb65d52 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Fri, 18 Oct 2024 11:02:12 -0300 Subject: [PATCH 05/19] [WIP] Add content negotiation for non-legacy characters in metric and label names Signed-off-by: Federico Torres --- src/encoding.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++ src/encoding/text.rs | 62 ++++++++++++++++++++++++------------ src/registry.rs | 14 +++++++- 3 files changed, 130 insertions(+), 22 deletions(-) diff --git a/src/encoding.rs b/src/encoding.rs index c644f82b..5bc4a751 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -11,6 +11,7 @@ use std::fmt::Write; use std::ops::Deref; use std::rc::Rc; use std::sync::Arc; +use crate::encoding::text::{is_valid_legacy_char, is_valid_legacy_metric_name}; #[cfg(feature = "protobuf")] #[cfg_attr(docsrs, doc(cfg(feature = "protobuf")))] @@ -747,3 +748,78 @@ impl<'a> From> for ExemplarValueEncoder<'a> { ExemplarValueEncoder(ExemplarValueEncoderInner::Protobuf(e)) } } + +#[derive(Debug, Default)] +pub enum EscapingScheme { + NoEscaping, + #[default] + UnderscoreEscaping, + DotsEscaping, + ValueEncodingEscaping, +} + +pub fn escape_name(name: &str, scheme: &EscapingScheme) -> String { + if name.is_empty() { + return name.to_string(); + } + let mut escaped = String::new(); + match scheme { + EscapingScheme::NoEscaping => return name.to_string(), + EscapingScheme::UnderscoreEscaping => { + if is_valid_legacy_metric_name(name) { + return name.to_string(); + } + for (i, b) in name.chars().enumerate() { + if is_valid_legacy_char(b, i) { + escaped.push(b); + } else { + escaped.push('_'); + } + } + } + EscapingScheme::DotsEscaping => { + for (i, b) in name.chars().enumerate() { + if b == '_' { + escaped.push_str("__"); + } else if b == '.' { + escaped.push_str("_dot_"); + } else if is_valid_legacy_char(b, i) { + escaped.push(b); + } else { + escaped.push('_'); + } + } + } + EscapingScheme::ValueEncodingEscaping => { + if is_valid_legacy_metric_name(name) { + return name.to_string(); + } + escaped.push_str("U__"); + for (i, b) in name.chars().enumerate() { + if is_valid_legacy_char(b, i) { + escaped.push(b); + } else if !b.is_ascii() { + escaped.push_str("_FFFD_"); + } else if b as u32 <= 0xFF { + write!(escaped, "_{:02X}_", b as u32).unwrap(); + } else if b as u32 <= 0xFFFF { + write!(escaped, "_{:04X}_", b as u32).unwrap(); + } + } + } + } + escaped +} + +pub fn negotiate(header: &str) -> EscapingScheme { + if header.contains("allow-utf-8") { + return EscapingScheme::NoEscaping; + } + if header.contains("dots") { + return EscapingScheme::DotsEscaping; + } + if header.contains("values") { + return EscapingScheme::ValueEncodingEscaping; + } + EscapingScheme::UnderscoreEscaping +} diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 5ac95612..11b0b05a 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -37,7 +37,7 @@ //! assert_eq!(expected_msg, buffer); //! ``` -use crate::encoding::{EncodeExemplarValue, EncodeLabelSet, NoLabelSet}; +use crate::encoding::{escape_name, EncodeExemplarValue, EncodeLabelSet, EscapingScheme, NoLabelSet}; use crate::metrics::exemplar::Exemplar; use crate::metrics::MetricType; use crate::registry::{Prefix, Registry, Unit}; @@ -150,7 +150,7 @@ pub fn encode_registry(writer: &mut W, registry: &Registry) -> Result<(), std where W: Write, { - registry.encode(&mut DescriptorEncoder::new(writer, ®istry.name_validation_scheme).into()) + registry.encode(&mut DescriptorEncoder::new(writer, ®istry.name_validation_scheme, ®istry.escaping_scheme).into()) } /// Encode the EOF marker into the provided [`Write`]r using the OpenMetrics @@ -195,6 +195,7 @@ pub(crate) struct DescriptorEncoder<'a> { prefix: Option<&'a Prefix>, labels: &'a [(Cow<'static, str>, Cow<'static, str>)], name_validation_scheme: &'a ValidationScheme, + escaping_scheme: &'a EscapingScheme, } impl<'a> std::fmt::Debug for DescriptorEncoder<'a> { @@ -204,12 +205,17 @@ impl<'a> std::fmt::Debug for DescriptorEncoder<'a> { } impl DescriptorEncoder<'_> { - pub(crate) fn new<'a>(writer: &'a mut dyn Write, name_validation_scheme: &'a ValidationScheme) -> DescriptorEncoder<'a> { + pub(crate) fn new<'a>( + writer: &'a mut dyn Write, + name_validation_scheme: &'a ValidationScheme, + escaping_scheme: &'a EscapingScheme, + ) -> DescriptorEncoder<'a> { DescriptorEncoder { writer, prefix: Default::default(), labels: Default::default(), name_validation_scheme, + escaping_scheme, } } @@ -223,6 +229,7 @@ impl DescriptorEncoder<'_> { labels, writer: self.writer, name_validation_scheme: &self.name_validation_scheme, + escaping_scheme: &self.escaping_scheme, } } @@ -238,10 +245,10 @@ impl DescriptorEncoder<'_> { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { - self.writer.write_str(prefix.as_str())?; + self.writer.write_str(escape_name(prefix.as_str(), self.escaping_scheme).as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(name)?; + self.writer.write_str(escape_name(name, self.escaping_scheme).as_str())?; if let Some(unit) = unit { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; @@ -258,10 +265,10 @@ impl DescriptorEncoder<'_> { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { - self.writer.write_str(prefix.as_str())?; + self.writer.write_str(escape_name(prefix.as_str(), self.escaping_scheme).as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(name)?; + self.writer.write_str(escape_name(name, self.escaping_scheme).as_str())?; if let Some(unit) = unit { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; @@ -279,10 +286,10 @@ impl DescriptorEncoder<'_> { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { - self.writer.write_str(prefix.as_str())?; + self.writer.write_str(escape_name(prefix.as_str(), self.escaping_scheme).as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(name)?; + self.writer.write_str(escape_name(name, self.escaping_scheme).as_str())?; self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { @@ -301,15 +308,16 @@ impl DescriptorEncoder<'_> { const_labels: self.labels, family_labels: None, name_validation_scheme: self.name_validation_scheme, + escaping_scheme: self.escaping_scheme, }) } } -fn is_valid_legacy_char(c: char, i: usize) -> bool { +pub fn is_valid_legacy_char(c: char, i: usize) -> bool { c.is_ascii_alphabetic() || c == '_' || c == ':' || (c.is_ascii_digit() && i > 0) } -fn is_valid_legacy_metric_name(name: &str) -> bool { +pub fn is_valid_legacy_metric_name(name: &str) -> bool { if name.is_empty() { return false; } @@ -372,13 +380,14 @@ pub(crate) struct MetricEncoder<'a> { const_labels: &'a [(Cow<'static, str>, Cow<'static, str>)], family_labels: Option<&'a dyn super::EncodeLabelSet>, name_validation_scheme: &'a ValidationScheme, + escaping_scheme: &'a EscapingScheme, } impl<'a> std::fmt::Debug for MetricEncoder<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut labels = String::new(); if let Some(l) = self.family_labels { - l.encode(LabelSetEncoder::new(&mut labels, self.name_validation_scheme).into())?; + l.encode(LabelSetEncoder::new(&mut labels, self.name_validation_scheme, self.escaping_scheme).into())?; } f.debug_struct("Encoder") @@ -474,6 +483,7 @@ impl<'a> MetricEncoder<'a> { const_labels: self.const_labels, family_labels: Some(label_set), name_validation_scheme: self.name_validation_scheme, + escaping_scheme: self.escaping_scheme, }) } @@ -533,7 +543,7 @@ impl<'a> MetricEncoder<'a> { self.writer.write_str(" # {")?; exemplar .label_set - .encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme).into())?; + .encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme, self.escaping_scheme).into())?; self.writer.write_str("} ")?; exemplar.value.encode( ExemplarValueEncoder { @@ -553,10 +563,10 @@ impl<'a> MetricEncoder<'a> { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { - self.writer.write_str(prefix.as_str())?; + self.writer.write_str(escape_name(prefix.as_str(), &self.escaping_scheme).as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(self.name)?; + self.writer.write_str(escape_name(self.name, &self.escaping_scheme).as_str())?; if let Some(unit) = self.unit { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; @@ -602,14 +612,14 @@ impl<'a> MetricEncoder<'a> { } self.const_labels - .encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme).into())?; + .encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme, self.escaping_scheme).into())?; if let Some(additional_labels) = additional_labels { if !self.const_labels.is_empty() { self.writer.write_str(",")?; } - additional_labels.encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme).into())?; + additional_labels.encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme, self.escaping_scheme).into())?; } /// Writer impl which prepends a comma on the first call to write output to the wrapped writer @@ -639,9 +649,9 @@ impl<'a> MetricEncoder<'a> { writer: self.writer, should_prepend: true, }; - labels.encode(LabelSetEncoder::new(&mut writer, self.name_validation_scheme).into())?; + labels.encode(LabelSetEncoder::new(&mut writer, self.name_validation_scheme, self.escaping_scheme).into())?; } else { - labels.encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme).into())?; + labels.encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme, self.escaping_scheme).into())?; }; } @@ -725,6 +735,7 @@ pub(crate) struct LabelSetEncoder<'a> { writer: &'a mut dyn Write, first: bool, name_validation_scheme: &'a ValidationScheme, + escaping_scheme: &'a EscapingScheme, } impl<'a> std::fmt::Debug for LabelSetEncoder<'a> { @@ -736,11 +747,16 @@ impl<'a> std::fmt::Debug for LabelSetEncoder<'a> { } impl<'a> LabelSetEncoder<'a> { - fn new(writer: &'a mut dyn Write, name_validation_scheme: &'a ValidationScheme) -> Self { + fn new( + writer: &'a mut dyn Write, + name_validation_scheme: &'a ValidationScheme, + escaping_scheme: &'a EscapingScheme, + ) -> Self { Self { writer, first: true, name_validation_scheme, + escaping_scheme, } } @@ -751,6 +767,7 @@ impl<'a> LabelSetEncoder<'a> { writer: self.writer, first, name_validation_scheme: self.name_validation_scheme, + escaping_scheme: self.escaping_scheme, } } } @@ -759,6 +776,7 @@ pub(crate) struct LabelEncoder<'a> { writer: &'a mut dyn Write, first: bool, name_validation_scheme: &'a ValidationScheme, + escaping_scheme: &'a EscapingScheme, } impl<'a> std::fmt::Debug for LabelEncoder<'a> { @@ -777,6 +795,7 @@ impl<'a> LabelEncoder<'a> { Ok(LabelKeyEncoder { writer: self.writer, name_validation_scheme: self.name_validation_scheme, + escaping_scheme: self.escaping_scheme, }) } } @@ -784,6 +803,7 @@ impl<'a> LabelEncoder<'a> { pub(crate) struct LabelKeyEncoder<'a> { writer: &'a mut dyn Write, name_validation_scheme: &'a ValidationScheme, + escaping_scheme: &'a EscapingScheme, } impl<'a> std::fmt::Debug for LabelKeyEncoder<'a> { @@ -806,7 +826,7 @@ impl<'a> std::fmt::Write for LabelKeyEncoder<'a> { if is_quoted_label_name(s, self.name_validation_scheme) { self.writer.write_str("\"")?; } - self.writer.write_str(s)?; + self.writer.write_str(escape_name(s, self.escaping_scheme).as_str())?; if is_quoted_label_name(s, self.name_validation_scheme) { self.writer.write_str("\"")?; } diff --git a/src/registry.rs b/src/registry.rs index d787b97a..684bba74 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; use crate::collector::Collector; -use crate::encoding::{DescriptorEncoder, EncodeMetric}; +use crate::encoding::{DescriptorEncoder, EncodeMetric, EscapingScheme}; use crate::encoding::text::ValidationScheme; /// A metric registry. @@ -66,6 +66,7 @@ pub struct Registry { collectors: Vec>, sub_registries: Vec, pub name_validation_scheme: ValidationScheme, + pub escaping_scheme: EscapingScheme, } impl Registry { @@ -107,6 +108,17 @@ impl Registry { ..Default::default() } } + + pub fn with_name_validation_scheme_and_escaping_scheme( + name_validation_scheme: ValidationScheme, + escaping_scheme: EscapingScheme + ) -> Self { + Self { + name_validation_scheme, + escaping_scheme, + ..Default::default() + } + } /// Register a metric with the [`Registry`]. From 4dc88590dc6acfd21cd53c5b69c82517795e5012 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Fri, 25 Oct 2024 11:53:51 -0300 Subject: [PATCH 06/19] Fix text encoding tests Signed-off-by: Federico Torres --- src/encoding/text.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 11b0b05a..66139544 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -869,6 +869,7 @@ mod tests { use std::borrow::Cow; use std::fmt::Error; use std::sync::atomic::{AtomicI32, AtomicU32}; + use crate::encoding::EscapingScheme::NoEscaping; #[test] fn encode_counter() { @@ -908,7 +909,7 @@ mod tests { #[test] fn encode_counter_with_unit_and_quoted_metric_name() { - let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); + let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, NoEscaping); let counter: Counter = Counter::default(); registry.register_with_unit("my.counter", "My counter", Unit::Seconds, counter); @@ -954,7 +955,7 @@ mod tests { #[test] fn encode_counter_with_exemplar_and_quoted_metric_name() { - let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); + let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, NoEscaping); let counter_with_exemplar: CounterWithExemplar> = CounterWithExemplar::default(); @@ -1052,7 +1053,7 @@ mod tests { #[test] fn encode_counter_family_with_prefix_with_label_with_quoted_metric_and_label_names() { - let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); + let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, EscapingScheme::NoEscaping); 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"))); @@ -1098,7 +1099,7 @@ mod tests { #[test] fn encode_info_with_quoted_metric_and_label_names() { - let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); + let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, NoEscaping); let info = Info::new(vec![("os.foo".to_string(), "GNU/linux".to_string())]); registry.register("my.info.metric", "My info metric", info); @@ -1204,7 +1205,7 @@ mod tests { #[test] fn encode_histogram_with_exemplars_and_quoted_metric_name() { - let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); + let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, NoEscaping); let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10)); registry.register("my.histogram", "My histogram", histogram.clone()); histogram.observe(1.0, Some([("user_id".to_string(), 42u64)])); @@ -1308,7 +1309,7 @@ mod tests { #[test] fn sub_registry_with_prefix_and_label_and_quoted_metric_and_label_names() { let top_level_metric_name = "my.top.level.metric"; - let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); + let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, NoEscaping); let counter: Counter = Counter::default(); registry.register(top_level_metric_name, "some help", counter.clone()); @@ -1468,7 +1469,7 @@ mod tests { } } - let mut registry = Registry::with_name_validation_scheme(ValidationScheme::UTF8Validation); + let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, NoEscaping); registry.register_collector(Box::new(Collector::new("top.level"))); let sub_registry = registry.sub_registry_with_prefix("prefix.1"); From ce522cdd7f288f7f9324b83b4b12d35450af603e Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Tue, 29 Oct 2024 10:52:42 -0300 Subject: [PATCH 07/19] Move name validation functions to encoding Signed-off-by: Federico Torres --- src/encoding.rs | 51 ++++++++++++++++++++++++++++++++++++- src/encoding/text.rs | 60 +------------------------------------------- src/registry.rs | 3 +-- 3 files changed, 52 insertions(+), 62 deletions(-) diff --git a/src/encoding.rs b/src/encoding.rs index 5bc4a751..08162797 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -11,7 +11,6 @@ use std::fmt::Write; use std::ops::Deref; use std::rc::Rc; use std::sync::Arc; -use crate::encoding::text::{is_valid_legacy_char, is_valid_legacy_metric_name}; #[cfg(feature = "protobuf")] #[cfg_attr(docsrs, doc(cfg(feature = "protobuf")))] @@ -749,6 +748,56 @@ impl<'a> From> for ExemplarValueEncoder<'a> { } } +#[derive(Debug, PartialEq, Default, Clone)] +pub enum ValidationScheme { + #[default] + LegacyValidation, + UTF8Validation, +} + +pub fn is_valid_legacy_char(c: char, i: usize) -> bool { + c.is_ascii_alphabetic() || c == '_' || c == ':' || (c.is_ascii_digit() && i > 0) +} + +pub fn is_valid_legacy_metric_name(name: &str) -> bool { + if name.is_empty() { + return false; + } + for (i, c) in name.chars().enumerate() { + if !is_valid_legacy_char(c, i) { + return false; + } + } + true +} + +fn is_valid_legacy_prefix(prefix: Option<&Prefix>) -> bool { + match prefix { + Some(prefix) => is_valid_legacy_metric_name(prefix.as_str()), + None => true, + } +} + +fn is_quoted_metric_name(name: &str, prefix: Option<&Prefix>, validation_scheme: &ValidationScheme) -> bool { + *validation_scheme == ValidationScheme::UTF8Validation && (!is_valid_legacy_metric_name(name) || !is_valid_legacy_prefix(prefix)) +} + +fn is_valid_legacy_label_name(label_name: &str) -> bool { + if label_name.is_empty() { + return false; + } + for (i, b) in label_name.chars().enumerate() { + if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { + return false; + } + } + true +} + +fn is_quoted_label_name(name: &str, validation_scheme: &ValidationScheme) -> bool { + *validation_scheme == ValidationScheme::UTF8Validation && !is_valid_legacy_label_name(name) +} + #[derive(Debug, Default)] pub enum EscapingScheme { NoEscaping, diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 66139544..84583651 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -37,23 +37,15 @@ //! assert_eq!(expected_msg, buffer); //! ``` -use crate::encoding::{escape_name, EncodeExemplarValue, EncodeLabelSet, EscapingScheme, NoLabelSet}; +use crate::encoding::{escape_name, is_quoted_label_name, is_quoted_metric_name, EncodeExemplarValue, EncodeLabelSet, EscapingScheme, NoLabelSet, ValidationScheme}; use crate::metrics::exemplar::Exemplar; use crate::metrics::MetricType; use crate::registry::{Prefix, Registry, Unit}; use std::borrow::Cow; -use std::cmp::PartialEq; use std::collections::HashMap; use std::fmt::Write; -#[derive(Debug, PartialEq, Default, Clone)] -pub enum ValidationScheme { - #[default] - LegacyValidation, - UTF8Validation, -} - /// Encode both the metrics registered with the provided [`Registry`] and the /// EOF marker into the provided [`Write`]r using the OpenMetrics text format. /// @@ -313,56 +305,6 @@ impl DescriptorEncoder<'_> { } } -pub fn is_valid_legacy_char(c: char, i: usize) -> bool { - c.is_ascii_alphabetic() || c == '_' || c == ':' || (c.is_ascii_digit() && i > 0) -} - -pub fn is_valid_legacy_metric_name(name: &str) -> bool { - if name.is_empty() { - return false; - } - for (i, c) in name.chars().enumerate() { - if !is_valid_legacy_char(c, i) { - return false; - } - } - true -} - -fn is_valid_legacy_prefix(prefix: Option<&Prefix>) -> bool { - match prefix { - Some(prefix) => is_valid_legacy_metric_name(prefix.as_str()), - None => true, - } -} - -/* -fn is_quoted_metric_name(name: &str, prefix: Option<&Prefix>) -> bool { - *NAME_VALIDATION_SCHEME.lock().unwrap() == ValidationScheme::UTF8Validation && (!is_valid_legacy_metric_name(name) || !is_valid_legacy_prefix(prefix)) -} - - */ - -fn is_quoted_metric_name(name: &str, prefix: Option<&Prefix>, validation_scheme: &ValidationScheme) -> bool { - *validation_scheme == ValidationScheme::UTF8Validation && (!is_valid_legacy_metric_name(name) || !is_valid_legacy_prefix(prefix)) -} - -fn is_valid_legacy_label_name(label_name: &str) -> bool { - if label_name.is_empty() { - return false; - } - for (i, b) in label_name.chars().enumerate() { - if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { - return false; - } - } - true -} - -fn is_quoted_label_name(name: &str, validation_scheme: &ValidationScheme) -> bool { - *validation_scheme == ValidationScheme::UTF8Validation && !is_valid_legacy_label_name(name) -} - /// Helper type for [`EncodeMetric`](super::EncodeMetric), see /// [`EncodeMetric::encode`](super::EncodeMetric::encode). /// diff --git a/src/registry.rs b/src/registry.rs index 684bba74..6b6c62c3 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -5,8 +5,7 @@ use std::borrow::Cow; use crate::collector::Collector; -use crate::encoding::{DescriptorEncoder, EncodeMetric, EscapingScheme}; -use crate::encoding::text::ValidationScheme; +use crate::encoding::{DescriptorEncoder, EncodeMetric, EscapingScheme, ValidationScheme}; /// A metric registry. /// From 81cd210cc098817df94a7b867ad859abb72f4c22 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Tue, 29 Oct 2024 16:51:47 -0300 Subject: [PATCH 08/19] Add getters for name_validation_scheme and escaping_scheme Signed-off-by: Federico Torres --- src/encoding.rs | 10 +++++----- src/encoding/text.rs | 2 +- src/registry.rs | 13 ++++++++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/encoding.rs b/src/encoding.rs index 08162797..bae39f1e 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -755,11 +755,11 @@ pub enum ValidationScheme { UTF8Validation, } -pub fn is_valid_legacy_char(c: char, i: usize) -> bool { +fn is_valid_legacy_char(c: char, i: usize) -> bool { c.is_ascii_alphabetic() || c == '_' || c == ':' || (c.is_ascii_digit() && i > 0) } -pub fn is_valid_legacy_metric_name(name: &str) -> bool { +fn is_valid_legacy_metric_name(name: &str) -> bool { if name.is_empty() { return false; } @@ -798,16 +798,16 @@ fn is_quoted_label_name(name: &str, validation_scheme: &ValidationScheme) -> boo *validation_scheme == ValidationScheme::UTF8Validation && !is_valid_legacy_label_name(name) } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub enum EscapingScheme { - NoEscaping, #[default] UnderscoreEscaping, DotsEscaping, ValueEncodingEscaping, + NoEscaping, } -pub fn escape_name(name: &str, scheme: &EscapingScheme) -> String { +fn escape_name(name: &str, scheme: &EscapingScheme) -> String { if name.is_empty() { return name.to_string(); } diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 84583651..59f32722 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -142,7 +142,7 @@ pub fn encode_registry(writer: &mut W, registry: &Registry) -> Result<(), std where W: Write, { - registry.encode(&mut DescriptorEncoder::new(writer, ®istry.name_validation_scheme, ®istry.escaping_scheme).into()) + registry.encode(&mut DescriptorEncoder::new(writer, ®istry.name_validation_scheme(), ®istry.escaping_scheme()).into()) } /// Encode the EOF marker into the provided [`Write`]r using the OpenMetrics diff --git a/src/registry.rs b/src/registry.rs index 6b6c62c3..4b925c36 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -64,8 +64,8 @@ pub struct Registry { metrics: Vec<(Descriptor, Box)>, collectors: Vec>, sub_registries: Vec, - pub name_validation_scheme: ValidationScheme, - pub escaping_scheme: EscapingScheme, + name_validation_scheme: ValidationScheme, + escaping_scheme: EscapingScheme, } impl Registry { @@ -118,7 +118,14 @@ impl Registry { ..Default::default() } } - + + pub(crate) fn name_validation_scheme(&self) -> ValidationScheme { + self.name_validation_scheme.clone() + } + + pub(crate) fn escaping_scheme(&self) -> EscapingScheme { + self.escaping_scheme.clone() + } /// Register a metric with the [`Registry`]. /// From 9eee74caefc6bdf65490e2262cd7060a1bd12ac4 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Tue, 29 Oct 2024 17:12:21 -0300 Subject: [PATCH 09/19] Add RegistryBuilder Signed-off-by: Federico Torres --- src/registry.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/registry.rs b/src/registry.rs index 4b925c36..3300c883 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -343,6 +343,55 @@ impl Registry { } } +#[derive(Debug, Default)] +pub struct RegistryBuilder { + prefix: Option, + labels: Vec<(Cow<'static, str>, Cow<'static, str>)>, + name_validation_scheme: ValidationScheme, + escaping_scheme: EscapingScheme, +} + +impl RegistryBuilder { + pub fn new() -> Self { + Self { + ..Default::default() + } + } + + pub fn with_prefix(mut self, prefix: impl Into) -> Self { + self.prefix = Some(Prefix(prefix.into())); + self + } + + pub fn with_labels( + mut self, + labels: impl Iterator, Cow<'static, str>)>, + ) -> Self { + self.labels = labels.into_iter().collect(); + self + } + + pub fn with_name_validation_scheme(mut self, name_validation_scheme: ValidationScheme) -> Self { + self.name_validation_scheme = name_validation_scheme; + self + } + + pub fn with_escaping_scheme(mut self, escaping_scheme: EscapingScheme) -> Self { + self.escaping_scheme = escaping_scheme; + self + } + + pub fn build(self) -> Registry { + Registry { + prefix: self.prefix, + labels: self.labels, + name_validation_scheme: self.name_validation_scheme, + escaping_scheme: self.escaping_scheme, + ..Default::default() + } + } +} + /// Metric prefix #[derive(Clone, Debug)] pub(crate) struct Prefix(String); From e25fceb536febaff9ca6339dfba1495a81c75261 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Thu, 31 Oct 2024 11:28:13 -0300 Subject: [PATCH 10/19] Add documentation Signed-off-by: Federico Torres --- src/encoding.rs | 16 ++++++++++++++++ src/lib.rs | 2 +- src/registry.rs | 28 +++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/encoding.rs b/src/encoding.rs index bae39f1e..d243fc58 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -748,10 +748,16 @@ impl<'a> From> for ExemplarValueEncoder<'a> { } } +/// Enum for determining how metric and label names will +/// be validated. #[derive(Debug, PartialEq, Default, Clone)] pub enum ValidationScheme { + /// Setting that requires that metric and label names + /// conform to the original OpenMetrics character requirements. #[default] LegacyValidation, + /// Only requires that metric and label names be valid UTF-8 + /// strings. UTF8Validation, } @@ -798,12 +804,21 @@ fn is_quoted_label_name(name: &str, validation_scheme: &ValidationScheme) -> boo *validation_scheme == ValidationScheme::UTF8Validation && !is_valid_legacy_label_name(name) } +/// Enum for determining how metric and label names will +/// be escaped. #[derive(Debug, Default, Clone)] pub enum EscapingScheme { + /// Replaces all legacy-invalid characters with underscores. #[default] UnderscoreEscaping, + /// Similar to UnderscoreEscaping, except that dots are + /// converted to `_dot_` and pre-existing underscores are converted to `__`. DotsEscaping, + /// Prepends the name with `U__` and replaces all invalid + /// characters with the Unicode value, surrounded by underscores. Single + /// underscores are replaced with double underscores. ValueEncodingEscaping, + /// Indicates that a name will not be escaped. NoEscaping, } @@ -860,6 +875,7 @@ fn escape_name(name: &str, scheme: &EscapingScheme) -> String { escaped } +/// Returns the escaping scheme to use based on the given header. pub fn negotiate(header: &str) -> EscapingScheme { if header.contains("allow-utf-8") { return EscapingScheme::NoEscaping; diff --git a/src/lib.rs b/src/lib.rs index cf8f0822..cfff6238 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ #![deny(dead_code)] -//#![deny(missing_docs)] +#![deny(missing_docs)] #![deny(unused)] #![forbid(unsafe_code)] #![warn(missing_debug_implementations)] diff --git a/src/registry.rs b/src/registry.rs index 3300c883..14d42ec4 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -98,7 +98,7 @@ impl Registry { ..Default::default() } } - + pub fn with_name_validation_scheme( name_validation_scheme: ValidationScheme ) -> Self { @@ -119,10 +119,12 @@ impl Registry { } } + /// Returns the given Registry's name validation scheme. pub(crate) fn name_validation_scheme(&self) -> ValidationScheme { self.name_validation_scheme.clone() } + /// Returns the given Registry's escaping scheme. pub(crate) fn escaping_scheme(&self) -> EscapingScheme { self.escaping_scheme.clone() } @@ -343,6 +345,24 @@ impl Registry { } } +/// A builder for creating a [`Registry`]. +/// +/// This struct allows for a more flexible and readable way to construct +/// a [`Registry`] by providing methods to set various parameters such as +/// prefix, labels, name validation scheme, and escaping scheme. +/// +/// ``` +/// # use prometheus_client::encoding::EscapingScheme::UnderscoreEscaping; +/// # use prometheus_client::encoding::ValidationScheme::{LegacyValidation, UTF8Validation}; +/// # use prometheus_client::registry::RegistryBuilder; +/// # +/// let registry = RegistryBuilder::new() +/// .with_prefix("my_prefix") +/// .with_labels(vec![("label1".into(), "value1".into())].into_iter()) +/// .with_name_validation_scheme(LegacyValidation) +/// .with_escaping_scheme(UnderscoreEscaping) +/// .build(); +/// ``` #[derive(Debug, Default)] pub struct RegistryBuilder { prefix: Option, @@ -352,17 +372,20 @@ pub struct RegistryBuilder { } impl RegistryBuilder { + /// Creates a new default ['RegistryBuilder']. pub fn new() -> Self { Self { ..Default::default() } } + /// Sets the prefix for the [`RegistryBuilder`]. pub fn with_prefix(mut self, prefix: impl Into) -> Self { self.prefix = Some(Prefix(prefix.into())); self } + /// Sets the labels for the [`RegistryBuilder`]. pub fn with_labels( mut self, labels: impl Iterator, Cow<'static, str>)>, @@ -371,16 +394,19 @@ impl RegistryBuilder { self } + /// Sets the name validation scheme for the [`RegistryBuilder`]. pub fn with_name_validation_scheme(mut self, name_validation_scheme: ValidationScheme) -> Self { self.name_validation_scheme = name_validation_scheme; self } + /// Sets the escaping scheme for the [`RegistryBuilder`]. pub fn with_escaping_scheme(mut self, escaping_scheme: EscapingScheme) -> Self { self.escaping_scheme = escaping_scheme; self } + /// Builds the [`Registry`] with the given parameters. pub fn build(self) -> Registry { Registry { prefix: self.prefix, From 37ca910e44b4d80e60c7eeccae3ca18f6cfb44e6 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Thu, 31 Oct 2024 11:33:39 -0300 Subject: [PATCH 11/19] Remove name validation scheme and escaping scheme constructors Signed-off-by: Federico Torres --- src/encoding/text.rs | 16 +++++++++------- src/registry.rs | 20 -------------------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 59f32722..a7e82a21 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -812,6 +812,8 @@ mod tests { use std::fmt::Error; use std::sync::atomic::{AtomicI32, AtomicU32}; use crate::encoding::EscapingScheme::NoEscaping; + use crate::encoding::ValidationScheme::UTF8Validation; + use crate::registry::RegistryBuilder; #[test] fn encode_counter() { @@ -851,7 +853,7 @@ mod tests { #[test] fn encode_counter_with_unit_and_quoted_metric_name() { - let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, NoEscaping); + let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); let counter: Counter = Counter::default(); registry.register_with_unit("my.counter", "My counter", Unit::Seconds, counter); @@ -897,7 +899,7 @@ mod tests { #[test] fn encode_counter_with_exemplar_and_quoted_metric_name() { - let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, NoEscaping); + let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); let counter_with_exemplar: CounterWithExemplar> = CounterWithExemplar::default(); @@ -995,7 +997,7 @@ mod tests { #[test] fn encode_counter_family_with_prefix_with_label_with_quoted_metric_and_label_names() { - let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, EscapingScheme::NoEscaping); + let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); 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"))); @@ -1041,7 +1043,7 @@ mod tests { #[test] fn encode_info_with_quoted_metric_and_label_names() { - let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, NoEscaping); + let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); let info = Info::new(vec![("os.foo".to_string(), "GNU/linux".to_string())]); registry.register("my.info.metric", "My info metric", info); @@ -1147,7 +1149,7 @@ mod tests { #[test] fn encode_histogram_with_exemplars_and_quoted_metric_name() { - let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, NoEscaping); + let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10)); registry.register("my.histogram", "My histogram", histogram.clone()); histogram.observe(1.0, Some([("user_id".to_string(), 42u64)])); @@ -1251,7 +1253,7 @@ mod tests { #[test] fn sub_registry_with_prefix_and_label_and_quoted_metric_and_label_names() { let top_level_metric_name = "my.top.level.metric"; - let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, NoEscaping); + let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); let counter: Counter = Counter::default(); registry.register(top_level_metric_name, "some help", counter.clone()); @@ -1411,7 +1413,7 @@ mod tests { } } - let mut registry = Registry::with_name_validation_scheme_and_escaping_scheme(ValidationScheme::UTF8Validation, NoEscaping); + let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); registry.register_collector(Box::new(Collector::new("top.level"))); let sub_registry = registry.sub_registry_with_prefix("prefix.1"); diff --git a/src/registry.rs b/src/registry.rs index 14d42ec4..9818c427 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -99,26 +99,6 @@ impl Registry { } } - pub fn with_name_validation_scheme( - name_validation_scheme: ValidationScheme - ) -> Self { - Self { - name_validation_scheme, - ..Default::default() - } - } - - pub fn with_name_validation_scheme_and_escaping_scheme( - name_validation_scheme: ValidationScheme, - escaping_scheme: EscapingScheme - ) -> Self { - Self { - name_validation_scheme, - escaping_scheme, - ..Default::default() - } - } - /// Returns the given Registry's name validation scheme. pub(crate) fn name_validation_scheme(&self) -> ValidationScheme { self.name_validation_scheme.clone() From 68d9ae9a7589dd7a80fe4da758b0ac1aa54a6e5e Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Tue, 5 Nov 2024 11:55:00 -0300 Subject: [PATCH 12/19] Fix metric and label name escaping when UTF-8 validation is enabled Signed-off-by: Federico Torres --- examples/axum-utf-8.rs | 101 +++++++++++++++++++++++++++++++++++++++++ src/encoding.rs | 23 ++++++++-- src/encoding/text.rs | 56 ++++++++++++++++------- src/registry.rs | 9 +++- 4 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 examples/axum-utf-8.rs diff --git a/examples/axum-utf-8.rs b/examples/axum-utf-8.rs new file mode 100644 index 00000000..15c8f0a0 --- /dev/null +++ b/examples/axum-utf-8.rs @@ -0,0 +1,101 @@ +use axum::body::Body; +use axum::extract::State; +use axum::http::header::CONTENT_TYPE; +use axum::http::{HeaderMap, StatusCode}; +use axum::response::{IntoResponse, Response}; +use axum::routing::get; +use axum::Router; +use prometheus_client::encoding::text::encode; +use prometheus_client::metrics::counter::Counter; +use prometheus_client::metrics::family::Family; +use prometheus_client::registry::{Registry, RegistryBuilder}; +use prometheus_client_derive_encode::{EncodeLabelSet, EncodeLabelValue}; +use std::sync::Arc; +use tokio::sync::Mutex; +use prometheus_client::encoding::EscapingScheme::UnderscoreEscaping; +use prometheus_client::encoding::negotiate_escaping_scheme; +use prometheus_client::encoding::ValidationScheme::UTF8Validation; + +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelValue)] +pub enum Method { + Get, + Post, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] +pub struct MethodLabels { + pub method: Method, +} + +#[derive(Debug)] +pub struct Metrics { + requests: Family, Counter>, +} + +impl Metrics { + pub fn inc_requests(&self, method: String) { + self.requests.get_or_create(&vec![("method.label".to_owned(), method)]).inc(); + } +} + +#[derive(Debug)] +pub struct AppState { + pub registry: Registry, +} + +pub async fn metrics_handler(State(state): State>>, headers: HeaderMap) -> impl IntoResponse { + let mut state = state.lock().await; + let mut buffer = String::new(); + if let Some(accept) = headers.get("Accept") { + let escaping_scheme = negotiate_escaping_scheme( + accept.to_str().unwrap(), + state.registry.escaping_scheme() + ); + state.registry.set_escaping_scheme(escaping_scheme); + } + encode(&mut buffer, &state.registry).unwrap(); + + Response::builder() + .status(StatusCode::OK) + .header( + CONTENT_TYPE, + "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=".to_owned() + state.registry.escaping_scheme().as_str(), + ) + .body(Body::from(buffer)) + .unwrap() +} + +pub async fn some_handler(State(metrics): State>>) -> impl IntoResponse { + metrics.lock().await.inc_requests("Get".to_owned()); + "okay".to_string() +} + +#[tokio::main] +async fn main() { + let metrics = Metrics { + requests: Family::default(), + }; + let mut state = AppState { + registry: RegistryBuilder::new() + .with_name_validation_scheme(UTF8Validation) + .with_escaping_scheme(UnderscoreEscaping) + .build(), + }; + state + .registry + .register("requests.count", "Count of requests", metrics.requests.clone()); + let metrics = Arc::new(Mutex::new(metrics)); + let state = Arc::new(Mutex::new(state)); + + let router = Router::new() + .route("/metrics", get(metrics_handler)) + .with_state(state) + .route("/handler", get(some_handler)) + .with_state(metrics); + let port = 8080; + let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) + .await + .unwrap(); + + axum::serve(listener, router).await.unwrap(); +} diff --git a/src/encoding.rs b/src/encoding.rs index d243fc58..d8b046b8 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -822,6 +822,18 @@ pub enum EscapingScheme { NoEscaping, } +impl EscapingScheme { + /// Returns a string representation of a `EscapingScheme`. + pub fn as_str(&self) -> &str { + match self { + EscapingScheme::UnderscoreEscaping => "underscores", + EscapingScheme::DotsEscaping => "dots", + EscapingScheme::ValueEncodingEscaping => "values", + EscapingScheme::NoEscaping => "allow-utf-8", + } + } +} + fn escape_name(name: &str, scheme: &EscapingScheme) -> String { if name.is_empty() { return name.to_string(); @@ -876,9 +888,9 @@ fn escape_name(name: &str, scheme: &EscapingScheme) -> String { } /// Returns the escaping scheme to use based on the given header. -pub fn negotiate(header: &str) -> EscapingScheme { - if header.contains("allow-utf-8") { - return EscapingScheme::NoEscaping; +pub fn negotiate_escaping_scheme(header: &str, default_escaping_scheme: EscapingScheme) -> EscapingScheme { + if header.contains("underscores") { + return EscapingScheme::UnderscoreEscaping; } if header.contains("dots") { return EscapingScheme::DotsEscaping; @@ -886,5 +898,8 @@ pub fn negotiate(header: &str) -> EscapingScheme { if header.contains("values") { return EscapingScheme::ValueEncodingEscaping; } - EscapingScheme::UnderscoreEscaping + if header.contains("allow-utf-8") { + return EscapingScheme::NoEscaping; + } + default_escaping_scheme } diff --git a/src/encoding/text.rs b/src/encoding/text.rs index a7e82a21..2c799d21 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -232,20 +232,28 @@ impl DescriptorEncoder<'_> { unit: Option<&'s Unit>, metric_type: MetricType, ) -> Result, std::fmt::Error> { + let escaped_name = escape_name(name, self.escaping_scheme); + let mut escaped_prefix: Option<&Prefix> = None; + let escaped_prefix_value: Prefix; + if let Some(prefix) = self.prefix { + escaped_prefix_value = Prefix::from(escape_name(prefix.as_str(), self.escaping_scheme)); + escaped_prefix = Some(&escaped_prefix_value); + } + let is_quoted_metric_name = is_quoted_metric_name(escaped_name.as_str(), escaped_prefix, self.name_validation_scheme); self.writer.write_str("# HELP ")?; - if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { + if is_quoted_metric_name { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { self.writer.write_str(escape_name(prefix.as_str(), self.escaping_scheme).as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(escape_name(name, self.escaping_scheme).as_str())?; + self.writer.write_str(escaped_name.as_str())?; if let Some(unit) = unit { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; } - if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { + if is_quoted_metric_name { self.writer.write_str("\"")?; } self.writer.write_str(" ")?; @@ -253,7 +261,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str("\n")?; self.writer.write_str("# TYPE ")?; - if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { + if is_quoted_metric_name { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { @@ -265,7 +273,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; } - if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { + if is_quoted_metric_name { self.writer.write_str("\"")?; } self.writer.write_str(" ")?; @@ -274,7 +282,7 @@ impl DescriptorEncoder<'_> { if let Some(unit) = unit { self.writer.write_str("# UNIT ")?; - if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { + if is_quoted_metric_name { self.writer.write_str("\"")?; } if let Some(prefix) = self.prefix { @@ -284,7 +292,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str(escape_name(name, self.escaping_scheme).as_str())?; self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; - if is_quoted_metric_name(name, self.prefix, self.name_validation_scheme) { + if is_quoted_metric_name { self.writer.write_str("\"")?; } self.writer.write_str(" ")?; @@ -354,8 +362,6 @@ impl<'a> MetricEncoder<'a> { ) -> Result<(), std::fmt::Error> { self.write_prefix_name_unit_suffix(Option::from("total"))?; - //self.write_suffix("total")?; - self.encode_labels::(None)?; v.encode( @@ -500,7 +506,15 @@ impl<'a> MetricEncoder<'a> { self.writer.write_str("\n") } fn write_prefix_name_unit_suffix(&mut self, suffix: Option<&'static str>) -> Result<(), std::fmt::Error> { - if is_quoted_metric_name(self.name, self.prefix, &self.name_validation_scheme) { + let escaped_name = escape_name(self.name, self.escaping_scheme); + let mut escaped_prefix: Option<&Prefix> = None; + let escaped_prefix_value: Prefix; + if let Some(prefix) = self.prefix { + escaped_prefix_value = Prefix::from(escape_name(prefix.as_str(), self.escaping_scheme)); + escaped_prefix = Some(&escaped_prefix_value); + } + let is_quoted_metric_name = is_quoted_metric_name(escaped_name.as_str(), escaped_prefix, self.name_validation_scheme); + if is_quoted_metric_name { self.writer.write_str("{")?; self.writer.write_str("\"")?; } @@ -517,7 +531,7 @@ impl<'a> MetricEncoder<'a> { self.writer.write_str("_")?; self.writer.write_str(suffix)?; } - if is_quoted_metric_name(self.name, self.prefix, &self.name_validation_scheme) { + if is_quoted_metric_name { self.writer.write_str("\"")?; } @@ -537,17 +551,25 @@ impl<'a> MetricEncoder<'a> { &mut self, additional_labels: Option<&S>, ) -> Result<(), std::fmt::Error> { + let escaped_name = escape_name(self.name, self.escaping_scheme); + let mut escaped_prefix: Option<&Prefix> = None; + let escaped_prefix_value: Prefix; + if let Some(prefix) = self.prefix { + escaped_prefix_value = Prefix::from(escape_name(prefix.as_str(), self.escaping_scheme)); + escaped_prefix = Some(&escaped_prefix_value); + } + let is_quoted_metric_name = is_quoted_metric_name(escaped_name.as_str(), escaped_prefix, self.name_validation_scheme); if self.const_labels.is_empty() && additional_labels.is_none() && self.family_labels.is_none() { - if is_quoted_metric_name(self.name, self.prefix, &self.name_validation_scheme) { + if is_quoted_metric_name { self.writer.write_str("}")?; } return Ok(()); } - if is_quoted_metric_name(self.name, self.prefix, &self.name_validation_scheme) { + if is_quoted_metric_name { self.writer.write_str(",")?; } else { self.writer.write_str("{")?; @@ -765,11 +787,13 @@ impl<'a> LabelKeyEncoder<'a> { impl<'a> std::fmt::Write for LabelKeyEncoder<'a> { fn write_str(&mut self, s: &str) -> std::fmt::Result { - if is_quoted_label_name(s, self.name_validation_scheme) { + let escaped_name = escape_name(s, self.escaping_scheme); + let is_quoted_label_name = is_quoted_label_name(escaped_name.as_str(), self.name_validation_scheme); + if is_quoted_label_name { self.writer.write_str("\"")?; } - self.writer.write_str(escape_name(s, self.escaping_scheme).as_str())?; - if is_quoted_label_name(s, self.name_validation_scheme) { + self.writer.write_str(escaped_name.as_str())?; + if is_quoted_label_name { self.writer.write_str("\"")?; } Ok(()) diff --git a/src/registry.rs b/src/registry.rs index 9818c427..2d082984 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -100,15 +100,20 @@ impl Registry { } /// Returns the given Registry's name validation scheme. - pub(crate) fn name_validation_scheme(&self) -> ValidationScheme { + pub fn name_validation_scheme(&self) -> ValidationScheme { self.name_validation_scheme.clone() } /// Returns the given Registry's escaping scheme. - pub(crate) fn escaping_scheme(&self) -> EscapingScheme { + pub fn escaping_scheme(&self) -> EscapingScheme { self.escaping_scheme.clone() } + /// Sets the escaping scheme for the [`RegistryBuilder`]. + pub fn set_escaping_scheme(&mut self, escaping_scheme: EscapingScheme) { + self.escaping_scheme = escaping_scheme; + } + /// Register a metric with the [`Registry`]. /// /// Note: In the Open Metrics text exposition format some metric types have From c0b0b418b35fdfe78a0f939a38c54661aa4131d0 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Tue, 5 Nov 2024 11:58:23 -0300 Subject: [PATCH 13/19] Remove commented code Signed-off-by: Federico Torres --- src/encoding/text.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 2c799d21..ecc790db 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -403,8 +403,6 @@ impl<'a> MetricEncoder<'a> { pub fn encode_info(&mut self, label_set: &S) -> Result<(), std::fmt::Error> { self.write_prefix_name_unit_suffix(Option::from("info"))?; - //self.write_suffix("info")?; - self.encode_labels(Some(label_set))?; self.writer.write_str(" ")?; @@ -443,14 +441,12 @@ impl<'a> MetricEncoder<'a> { exemplars: Option<&HashMap>>, ) -> Result<(), std::fmt::Error> { self.write_prefix_name_unit_suffix(Option::from("sum"))?; - //self.write_suffix("sum")?; self.encode_labels::(None)?; self.writer.write_str(" ")?; self.writer.write_str(dtoa::Buffer::new().format(sum))?; self.newline()?; self.write_prefix_name_unit_suffix(Option::from("count"))?; - //self.write_suffix("count")?; self.encode_labels::(None)?; self.writer.write_str(" ")?; self.writer.write_str(itoa::Buffer::new().format(count))?; @@ -461,7 +457,6 @@ impl<'a> MetricEncoder<'a> { cummulative += count; self.write_prefix_name_unit_suffix(Option::from("bucket"))?; - //self.write_suffix("bucket")?; if *upper_bound == f64::MAX { self.encode_labels(Some(&[("le", "+Inf")]))?; @@ -538,13 +533,6 @@ impl<'a> MetricEncoder<'a> { Ok(()) } -/* fn write_suffix(&mut self, suffix: &'static str) -> Result<(), std::fmt::Error> { - self.writer.write_str("_")?; - self.writer.write_str(suffix)?; - - Ok(()) - }*/ - // TODO: Consider caching the encoded labels for Histograms as they stay the // same but are currently encoded multiple times. fn encode_labels( From 8b85cdb3539be88a1859b449a3d32af0bc12478f Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Tue, 5 Nov 2024 12:00:49 -0300 Subject: [PATCH 14/19] Remove unused structs in axum UTF-8 example Signed-off-by: Federico Torres --- examples/axum-utf-8.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/examples/axum-utf-8.rs b/examples/axum-utf-8.rs index 15c8f0a0..01f1d0cc 100644 --- a/examples/axum-utf-8.rs +++ b/examples/axum-utf-8.rs @@ -9,24 +9,12 @@ use prometheus_client::encoding::text::encode; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::registry::{Registry, RegistryBuilder}; -use prometheus_client_derive_encode::{EncodeLabelSet, EncodeLabelValue}; use std::sync::Arc; use tokio::sync::Mutex; use prometheus_client::encoding::EscapingScheme::UnderscoreEscaping; use prometheus_client::encoding::negotiate_escaping_scheme; use prometheus_client::encoding::ValidationScheme::UTF8Validation; -#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelValue)] -pub enum Method { - Get, - Post, -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] -pub struct MethodLabels { - pub method: Method, -} - #[derive(Debug)] pub struct Metrics { requests: Family, Counter>, From 4b1ce0086d91b2e2492f1a348bc6b258884c06d4 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Tue, 5 Nov 2024 12:12:36 -0300 Subject: [PATCH 15/19] Remove unnecessary escape_name calls Signed-off-by: Federico Torres --- src/encoding/text.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/encoding/text.rs b/src/encoding/text.rs index ecc790db..85c532b8 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -244,8 +244,8 @@ impl DescriptorEncoder<'_> { if is_quoted_metric_name { self.writer.write_str("\"")?; } - if let Some(prefix) = self.prefix { - self.writer.write_str(escape_name(prefix.as_str(), self.escaping_scheme).as_str())?; + if let Some(prefix) = escaped_prefix { + self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; } self.writer.write_str(escaped_name.as_str())?; @@ -264,11 +264,11 @@ impl DescriptorEncoder<'_> { if is_quoted_metric_name { self.writer.write_str("\"")?; } - if let Some(prefix) = self.prefix { - self.writer.write_str(escape_name(prefix.as_str(), self.escaping_scheme).as_str())?; + if let Some(prefix) = escaped_prefix { + self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(escape_name(name, self.escaping_scheme).as_str())?; + self.writer.write_str(escaped_name.as_str())?; if let Some(unit) = unit { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; @@ -285,11 +285,11 @@ impl DescriptorEncoder<'_> { if is_quoted_metric_name { self.writer.write_str("\"")?; } - if let Some(prefix) = self.prefix { - self.writer.write_str(escape_name(prefix.as_str(), self.escaping_scheme).as_str())?; + if let Some(prefix) = escaped_prefix { + self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(escape_name(name, self.escaping_scheme).as_str())?; + self.writer.write_str(escaped_name.as_str())?; self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; if is_quoted_metric_name { @@ -513,11 +513,11 @@ impl<'a> MetricEncoder<'a> { self.writer.write_str("{")?; self.writer.write_str("\"")?; } - if let Some(prefix) = self.prefix { - self.writer.write_str(escape_name(prefix.as_str(), &self.escaping_scheme).as_str())?; + if let Some(prefix) = escaped_prefix { + self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(escape_name(self.name, &self.escaping_scheme).as_str())?; + self.writer.write_str(escaped_name.as_str())?; if let Some(unit) = self.unit { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; From b0d7ae2aff2ec3dd63aed706d8c5a37ed27d8fbb Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Thu, 7 Nov 2024 16:35:12 -0300 Subject: [PATCH 16/19] Formatting Signed-off-by: Federico Torres --- examples/axum-utf-8.rs | 32 +++++---- src/encoding.rs | 20 ++++-- src/encoding/text.rs | 144 ++++++++++++++++++++++++++++++++--------- 3 files changed, 150 insertions(+), 46 deletions(-) diff --git a/examples/axum-utf-8.rs b/examples/axum-utf-8.rs index 01f1d0cc..0c6a1e49 100644 --- a/examples/axum-utf-8.rs +++ b/examples/axum-utf-8.rs @@ -5,15 +5,15 @@ use axum::http::{HeaderMap, StatusCode}; use axum::response::{IntoResponse, Response}; use axum::routing::get; use axum::Router; +use prometheus_client::encoding::negotiate_escaping_scheme; use prometheus_client::encoding::text::encode; +use prometheus_client::encoding::EscapingScheme::UnderscoreEscaping; +use prometheus_client::encoding::ValidationScheme::UTF8Validation; use prometheus_client::metrics::counter::Counter; use prometheus_client::metrics::family::Family; use prometheus_client::registry::{Registry, RegistryBuilder}; use std::sync::Arc; use tokio::sync::Mutex; -use prometheus_client::encoding::EscapingScheme::UnderscoreEscaping; -use prometheus_client::encoding::negotiate_escaping_scheme; -use prometheus_client::encoding::ValidationScheme::UTF8Validation; #[derive(Debug)] pub struct Metrics { @@ -22,7 +22,9 @@ pub struct Metrics { impl Metrics { pub fn inc_requests(&self, method: String) { - self.requests.get_or_create(&vec![("method.label".to_owned(), method)]).inc(); + self.requests + .get_or_create(&vec![("method.label".to_owned(), method)]) + .inc(); } } @@ -31,14 +33,15 @@ pub struct AppState { pub registry: Registry, } -pub async fn metrics_handler(State(state): State>>, headers: HeaderMap) -> impl IntoResponse { +pub async fn metrics_handler( + State(state): State>>, + headers: HeaderMap, +) -> impl IntoResponse { let mut state = state.lock().await; let mut buffer = String::new(); if let Some(accept) = headers.get("Accept") { - let escaping_scheme = negotiate_escaping_scheme( - accept.to_str().unwrap(), - state.registry.escaping_scheme() - ); + let escaping_scheme = + negotiate_escaping_scheme(accept.to_str().unwrap(), state.registry.escaping_scheme()); state.registry.set_escaping_scheme(escaping_scheme); } encode(&mut buffer, &state.registry).unwrap(); @@ -47,7 +50,8 @@ pub async fn metrics_handler(State(state): State>>, headers: .status(StatusCode::OK) .header( CONTENT_TYPE, - "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=".to_owned() + state.registry.escaping_scheme().as_str(), + "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=".to_owned() + + state.registry.escaping_scheme().as_str(), ) .body(Body::from(buffer)) .unwrap() @@ -69,9 +73,11 @@ async fn main() { .with_escaping_scheme(UnderscoreEscaping) .build(), }; - state - .registry - .register("requests.count", "Count of requests", metrics.requests.clone()); + state.registry.register( + "requests.count", + "Count of requests", + metrics.requests.clone(), + ); let metrics = Arc::new(Mutex::new(metrics)); let state = Arc::new(Mutex::new(state)); diff --git a/src/encoding.rs b/src/encoding.rs index 525441a3..d8bf53bd 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -797,8 +797,13 @@ fn is_valid_legacy_prefix(prefix: Option<&Prefix>) -> bool { } } -fn is_quoted_metric_name(name: &str, prefix: Option<&Prefix>, validation_scheme: &ValidationScheme) -> bool { - *validation_scheme == ValidationScheme::UTF8Validation && (!is_valid_legacy_metric_name(name) || !is_valid_legacy_prefix(prefix)) +fn is_quoted_metric_name( + name: &str, + prefix: Option<&Prefix>, + validation_scheme: &ValidationScheme, +) -> bool { + *validation_scheme == ValidationScheme::UTF8Validation + && (!is_valid_legacy_metric_name(name) || !is_valid_legacy_prefix(prefix)) } fn is_valid_legacy_label_name(label_name: &str) -> bool { @@ -806,7 +811,11 @@ fn is_valid_legacy_label_name(label_name: &str) -> bool { return false; } for (i, b) in label_name.chars().enumerate() { - if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { + if !((b >= 'a' && b <= 'z') + || (b >= 'A' && b <= 'Z') + || b == '_' + || (b >= '0' && b <= '9' && i > 0)) + { return false; } } @@ -901,7 +910,10 @@ fn escape_name(name: &str, scheme: &EscapingScheme) -> String { } /// Returns the escaping scheme to use based on the given header. -pub fn negotiate_escaping_scheme(header: &str, default_escaping_scheme: EscapingScheme) -> EscapingScheme { +pub fn negotiate_escaping_scheme( + header: &str, + default_escaping_scheme: EscapingScheme, +) -> EscapingScheme { if header.contains("underscores") { return EscapingScheme::UnderscoreEscaping; } diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 85c532b8..9eb28dfc 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -37,7 +37,10 @@ //! assert_eq!(expected_msg, buffer); //! ``` -use crate::encoding::{escape_name, is_quoted_label_name, is_quoted_metric_name, EncodeExemplarValue, EncodeLabelSet, EscapingScheme, NoLabelSet, ValidationScheme}; +use crate::encoding::{ + escape_name, is_quoted_label_name, is_quoted_metric_name, EncodeExemplarValue, EncodeLabelSet, + EscapingScheme, NoLabelSet, ValidationScheme, +}; use crate::metrics::exemplar::Exemplar; use crate::metrics::MetricType; use crate::registry::{Prefix, Registry, Unit}; @@ -142,7 +145,14 @@ pub fn encode_registry(writer: &mut W, registry: &Registry) -> Result<(), std where W: Write, { - registry.encode(&mut DescriptorEncoder::new(writer, ®istry.name_validation_scheme(), ®istry.escaping_scheme()).into()) + registry.encode( + &mut DescriptorEncoder::new( + writer, + ®istry.name_validation_scheme(), + ®istry.escaping_scheme(), + ) + .into(), + ) } /// Encode the EOF marker into the provided [`Write`]r using the OpenMetrics @@ -198,7 +208,7 @@ impl<'a> std::fmt::Debug for DescriptorEncoder<'a> { impl DescriptorEncoder<'_> { pub(crate) fn new<'a>( - writer: &'a mut dyn Write, + writer: &'a mut dyn Write, name_validation_scheme: &'a ValidationScheme, escaping_scheme: &'a EscapingScheme, ) -> DescriptorEncoder<'a> { @@ -239,7 +249,11 @@ impl DescriptorEncoder<'_> { escaped_prefix_value = Prefix::from(escape_name(prefix.as_str(), self.escaping_scheme)); escaped_prefix = Some(&escaped_prefix_value); } - let is_quoted_metric_name = is_quoted_metric_name(escaped_name.as_str(), escaped_prefix, self.name_validation_scheme); + let is_quoted_metric_name = is_quoted_metric_name( + escaped_name.as_str(), + escaped_prefix, + self.name_validation_scheme, + ); self.writer.write_str("# HELP ")?; if is_quoted_metric_name { self.writer.write_str("\"")?; @@ -337,7 +351,14 @@ impl<'a> std::fmt::Debug for MetricEncoder<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut labels = String::new(); if let Some(l) = self.family_labels { - l.encode(LabelSetEncoder::new(&mut labels, self.name_validation_scheme, self.escaping_scheme).into())?; + l.encode( + LabelSetEncoder::new( + &mut labels, + self.name_validation_scheme, + self.escaping_scheme, + ) + .into(), + )?; } f.debug_struct("Encoder") @@ -484,9 +505,14 @@ impl<'a> MetricEncoder<'a> { exemplar: &Exemplar, ) -> Result<(), std::fmt::Error> { self.writer.write_str(" # {")?; - exemplar - .label_set - .encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme, self.escaping_scheme).into())?; + exemplar.label_set.encode( + LabelSetEncoder::new( + self.writer, + self.name_validation_scheme, + self.escaping_scheme, + ) + .into(), + )?; self.writer.write_str("} ")?; exemplar.value.encode( ExemplarValueEncoder { @@ -500,7 +526,10 @@ impl<'a> MetricEncoder<'a> { fn newline(&mut self) -> Result<(), std::fmt::Error> { self.writer.write_str("\n") } - fn write_prefix_name_unit_suffix(&mut self, suffix: Option<&'static str>) -> Result<(), std::fmt::Error> { + fn write_prefix_name_unit_suffix( + &mut self, + suffix: Option<&'static str>, + ) -> Result<(), std::fmt::Error> { let escaped_name = escape_name(self.name, self.escaping_scheme); let mut escaped_prefix: Option<&Prefix> = None; let escaped_prefix_value: Prefix; @@ -508,7 +537,11 @@ impl<'a> MetricEncoder<'a> { escaped_prefix_value = Prefix::from(escape_name(prefix.as_str(), self.escaping_scheme)); escaped_prefix = Some(&escaped_prefix_value); } - let is_quoted_metric_name = is_quoted_metric_name(escaped_name.as_str(), escaped_prefix, self.name_validation_scheme); + let is_quoted_metric_name = is_quoted_metric_name( + escaped_name.as_str(), + escaped_prefix, + self.name_validation_scheme, + ); if is_quoted_metric_name { self.writer.write_str("{")?; self.writer.write_str("\"")?; @@ -546,7 +579,11 @@ impl<'a> MetricEncoder<'a> { escaped_prefix_value = Prefix::from(escape_name(prefix.as_str(), self.escaping_scheme)); escaped_prefix = Some(&escaped_prefix_value); } - let is_quoted_metric_name = is_quoted_metric_name(escaped_name.as_str(), escaped_prefix, self.name_validation_scheme); + let is_quoted_metric_name = is_quoted_metric_name( + escaped_name.as_str(), + escaped_prefix, + self.name_validation_scheme, + ); if self.const_labels.is_empty() && additional_labels.is_none() && self.family_labels.is_none() @@ -563,15 +600,28 @@ impl<'a> MetricEncoder<'a> { self.writer.write_str("{")?; } - self.const_labels - .encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme, self.escaping_scheme).into())?; + self.const_labels.encode( + LabelSetEncoder::new( + self.writer, + self.name_validation_scheme, + self.escaping_scheme, + ) + .into(), + )?; if let Some(additional_labels) = additional_labels { if !self.const_labels.is_empty() { self.writer.write_str(",")?; } - additional_labels.encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme, self.escaping_scheme).into())?; + additional_labels.encode( + LabelSetEncoder::new( + self.writer, + self.name_validation_scheme, + self.escaping_scheme, + ) + .into(), + )?; } /// Writer impl which prepends a comma on the first call to write output to the wrapped writer @@ -601,9 +651,23 @@ impl<'a> MetricEncoder<'a> { writer: self.writer, should_prepend: true, }; - labels.encode(LabelSetEncoder::new(&mut writer, self.name_validation_scheme, self.escaping_scheme).into())?; + labels.encode( + LabelSetEncoder::new( + &mut writer, + self.name_validation_scheme, + self.escaping_scheme, + ) + .into(), + )?; } else { - labels.encode(LabelSetEncoder::new(self.writer, self.name_validation_scheme, self.escaping_scheme).into())?; + labels.encode( + LabelSetEncoder::new( + self.writer, + self.name_validation_scheme, + self.escaping_scheme, + ) + .into(), + )?; }; } @@ -700,8 +764,8 @@ impl<'a> std::fmt::Debug for LabelSetEncoder<'a> { impl<'a> LabelSetEncoder<'a> { fn new( - writer: &'a mut dyn Write, - name_validation_scheme: &'a ValidationScheme, + writer: &'a mut dyn Write, + name_validation_scheme: &'a ValidationScheme, escaping_scheme: &'a EscapingScheme, ) -> Self { Self { @@ -776,7 +840,8 @@ impl<'a> LabelKeyEncoder<'a> { impl<'a> std::fmt::Write for LabelKeyEncoder<'a> { fn write_str(&mut self, s: &str) -> std::fmt::Result { let escaped_name = escape_name(s, self.escaping_scheme); - let is_quoted_label_name = is_quoted_label_name(escaped_name.as_str(), self.name_validation_scheme); + let is_quoted_label_name = + is_quoted_label_name(escaped_name.as_str(), self.name_validation_scheme); if is_quoted_label_name { self.writer.write_str("\"")?; } @@ -813,19 +878,19 @@ impl<'a> std::fmt::Write for LabelValueEncoder<'a> { #[cfg(test)] mod tests { use super::*; + use crate::encoding::EscapingScheme::NoEscaping; + use crate::encoding::ValidationScheme::UTF8Validation; use crate::metrics::exemplar::HistogramWithExemplars; use crate::metrics::family::Family; use crate::metrics::gauge::Gauge; use crate::metrics::histogram::{exponential_buckets, Histogram}; use crate::metrics::info::Info; use crate::metrics::{counter::Counter, exemplar::CounterWithExemplar}; + use crate::registry::RegistryBuilder; use pyo3::{prelude::*, types::PyModule}; use std::borrow::Cow; use std::fmt::Error; use std::sync::atomic::{AtomicI32, AtomicU32}; - use crate::encoding::EscapingScheme::NoEscaping; - use crate::encoding::ValidationScheme::UTF8Validation; - use crate::registry::RegistryBuilder; #[test] fn encode_counter() { @@ -865,7 +930,10 @@ mod tests { #[test] fn encode_counter_with_unit_and_quoted_metric_name() { - let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); + let mut registry = RegistryBuilder::new() + .with_name_validation_scheme(UTF8Validation) + .with_escaping_scheme(NoEscaping) + .build(); let counter: Counter = Counter::default(); registry.register_with_unit("my.counter", "My counter", Unit::Seconds, counter); @@ -911,7 +979,10 @@ mod tests { #[test] fn encode_counter_with_exemplar_and_quoted_metric_name() { - let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); + let mut registry = RegistryBuilder::new() + .with_name_validation_scheme(UTF8Validation) + .with_escaping_scheme(NoEscaping) + .build(); let counter_with_exemplar: CounterWithExemplar> = CounterWithExemplar::default(); @@ -1009,7 +1080,10 @@ mod tests { #[test] fn encode_counter_family_with_prefix_with_label_with_quoted_metric_and_label_names() { - let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); + let mut registry = RegistryBuilder::new() + .with_name_validation_scheme(UTF8Validation) + .with_escaping_scheme(NoEscaping) + .build(); 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"))); @@ -1055,7 +1129,10 @@ mod tests { #[test] fn encode_info_with_quoted_metric_and_label_names() { - let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); + let mut registry = RegistryBuilder::new() + .with_name_validation_scheme(UTF8Validation) + .with_escaping_scheme(NoEscaping) + .build(); let info = Info::new(vec![("os.foo".to_string(), "GNU/linux".to_string())]); registry.register("my.info.metric", "My info metric", info); @@ -1161,7 +1238,10 @@ mod tests { #[test] fn encode_histogram_with_exemplars_and_quoted_metric_name() { - let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); + let mut registry = RegistryBuilder::new() + .with_name_validation_scheme(UTF8Validation) + .with_escaping_scheme(NoEscaping) + .build(); let histogram = HistogramWithExemplars::new(exponential_buckets(1.0, 2.0, 10)); registry.register("my.histogram", "My histogram", histogram.clone()); histogram.observe(1.0, Some([("user_id".to_string(), 42u64)])); @@ -1265,7 +1345,10 @@ mod tests { #[test] fn sub_registry_with_prefix_and_label_and_quoted_metric_and_label_names() { let top_level_metric_name = "my.top.level.metric"; - let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); + let mut registry = RegistryBuilder::new() + .with_name_validation_scheme(UTF8Validation) + .with_escaping_scheme(NoEscaping) + .build(); let counter: Counter = Counter::default(); registry.register(top_level_metric_name, "some help", counter.clone()); @@ -1425,7 +1508,10 @@ mod tests { } } - let mut registry = RegistryBuilder::new().with_name_validation_scheme(UTF8Validation).with_escaping_scheme(NoEscaping).build(); + let mut registry = RegistryBuilder::new() + .with_name_validation_scheme(UTF8Validation) + .with_escaping_scheme(NoEscaping) + .build(); registry.register_collector(Box::new(Collector::new("top.level"))); let sub_registry = registry.sub_registry_with_prefix("prefix.1"); From 598fe971aa8b78c0f11b2439d7570db0782cc9b3 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Thu, 16 Jan 2025 15:16:33 -0300 Subject: [PATCH 17/19] Add more test cases Signed-off-by: Federico Torres --- src/encoding.rs | 156 ++++++++++++++++++++++++++++++++++++++++--- src/encoding/text.rs | 60 +++++++++++++++++ 2 files changed, 208 insertions(+), 8 deletions(-) diff --git a/src/encoding.rs b/src/encoding.rs index 85b13dc3..448cf626 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -891,7 +891,7 @@ fn escape_name(name: &str, scheme: &EscapingScheme) -> String { } else if is_valid_legacy_char(b, i) { escaped.push(b); } else { - escaped.push('_'); + escaped.push_str("__"); } } } @@ -901,14 +901,12 @@ fn escape_name(name: &str, scheme: &EscapingScheme) -> String { } escaped.push_str("U__"); for (i, b) in name.chars().enumerate() { - if is_valid_legacy_char(b, i) { + if b == '_' { + escaped.push_str("__"); + } else if is_valid_legacy_char(b, i) { escaped.push(b); - } else if !b.is_ascii() { - escaped.push_str("_FFFD_"); - } else if b as u32 <= 0xFF { - write!(escaped, "_{:02X}_", b as u32).unwrap(); - } else if b as u32 <= 0xFFFF { - write!(escaped, "_{:04X}_", b as u32).unwrap(); + } else { + write!(escaped, "_{:x}_", b as i64).unwrap(); } } } @@ -935,3 +933,145 @@ pub fn negotiate_escaping_scheme( } default_escaping_scheme } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn metric_name_is_legacy_valid() { + struct Scenario { + input: &'static str, + expected: bool, + } + + let scenarios = vec![ + Scenario { + input: "Avalid_23name", + expected: true, + }, + Scenario { + input: "_Avalid_23name", + expected: true, + }, + Scenario { + input: "1valid_23name", + expected: false, + }, + Scenario { + input: "avalid_23name", + expected: true, + }, + Scenario { + input: "Ava:lid_23name", + expected: true, + }, + Scenario { + input: "a lid_23name", + expected: false, + }, + Scenario { + input: ":leading_colon", + expected: true, + }, + Scenario { + input: "colon:in:the:middle", + expected: true, + }, + Scenario { + input: "", + expected: false, + }, + Scenario { + input: "aÅz", + expected: false, + }, + ]; + + for scenario in scenarios { + let result = is_valid_legacy_metric_name(scenario.input); + assert_eq!(result, scenario.expected); + } + } + + #[test] + fn test_escape_name() { + struct Scenario { + name: &'static str, + input: &'static str, + expected_underscores: &'static str, + expected_dots: &'static str, + expected_value: &'static str, + } + + let scenarios = vec![ + Scenario { + name: "empty string", + input: "", + expected_underscores: "", + expected_dots: "", + expected_value: "", + }, + Scenario { + name: "legacy valid name", + input: "no:escaping_required", + expected_underscores: "no:escaping_required", + expected_dots: "no:escaping__required", + expected_value: "no:escaping_required", + }, + Scenario { + name: "name with dots", + input: "mysystem.prod.west.cpu.load", + expected_underscores: "mysystem_prod_west_cpu_load", + expected_dots: "mysystem_dot_prod_dot_west_dot_cpu_dot_load", + expected_value: "U__mysystem_2e_prod_2e_west_2e_cpu_2e_load", + }, + Scenario { + name: "name with dots and underscore", + input: "mysystem.prod.west.cpu.load_total", + expected_underscores: "mysystem_prod_west_cpu_load_total", + expected_dots: "mysystem_dot_prod_dot_west_dot_cpu_dot_load__total", + expected_value: "U__mysystem_2e_prod_2e_west_2e_cpu_2e_load__total", + }, + Scenario { + name: "name with dots and colon", + input: "http.status:sum", + expected_underscores: "http_status:sum", + expected_dots: "http_dot_status:sum", + expected_value: "U__http_2e_status:sum", + }, + Scenario { + name: "name with spaces and emoji", + input: "label with 😱", + expected_underscores: "label_with__", + expected_dots: "label__with____", + expected_value: "U__label_20_with_20__1f631_", + }, + Scenario { + name: "name with unicode characters > 0x100", + input: "花火", + expected_underscores: "__", + expected_dots: "____", + expected_value: "U___82b1__706b_", + }, + Scenario { + name: "name with spaces and edge-case value", + input: "label with \u{0100}", + expected_underscores: "label_with__", + expected_dots: "label__with____", + expected_value: "U__label_20_with_20__100_", + }, + ]; + + for scenario in scenarios { + let result = escape_name(scenario.input, &EscapingScheme::UnderscoreEscaping); + assert_eq!(result, scenario.expected_underscores, "{}", scenario.name); + + let result = escape_name(scenario.input, &EscapingScheme::DotsEscaping); + assert_eq!(result, scenario.expected_dots, "{}", scenario.name); + + let result = escape_name(scenario.input, &EscapingScheme::ValueEncodingEscaping); + assert_eq!(result, scenario.expected_value, "{}", scenario.name); + } + } +} diff --git a/src/encoding/text.rs b/src/encoding/text.rs index 605b21d0..cef5ec19 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -1582,6 +1582,66 @@ mod tests { assert_eq!(&response[response.len() - 20..], "ogins_total 0\n# EOF\n"); } + #[test] + fn encode_labels_no_labels() { + let mut buffer = String::new(); + let mut encoder = MetricEncoder { + writer: &mut buffer, + prefix: None, + name: "name", + unit: None, + const_labels: &[], + family_labels: None, + name_validation_scheme: &UTF8Validation, + escaping_scheme: &NoEscaping, + }; + + encoder.encode_labels::(None).unwrap(); + assert_eq!(buffer, ""); + } + + #[test] + fn encode_labels_with_all_labels() { + let mut buffer = String::new(); + let const_labels = vec![(Cow::Borrowed("t1"), Cow::Borrowed("t1"))]; + let additional_labels = vec![(Cow::Borrowed("t2"), Cow::Borrowed("t2"))]; + let family_labels = vec![(Cow::Borrowed("t3"), Cow::Borrowed("t3"))]; + let mut encoder = MetricEncoder { + writer: &mut buffer, + prefix: None, + name: "name", + unit: None, + const_labels: &const_labels, + family_labels: Some(&family_labels), + name_validation_scheme: &UTF8Validation, + escaping_scheme: &NoEscaping, + }; + + encoder.encode_labels(Some(&additional_labels)).unwrap(); + assert_eq!(buffer, "{t1=\"t1\",t2=\"t2\",t3=\"t3\"}"); + } + + #[test] + fn encode_labels_with_quoted_label_names() { + let mut buffer = String::new(); + let const_labels = vec![(Cow::Borrowed("service.name"), Cow::Borrowed("t1"))]; + let additional_labels = vec![(Cow::Borrowed("whatever\\whatever"), Cow::Borrowed("t2"))]; + let family_labels = vec![(Cow::Borrowed("t*3"), Cow::Borrowed("t3"))]; + let mut encoder = MetricEncoder { + writer: &mut buffer, + prefix: None, + name: "name", + unit: None, + const_labels: &const_labels, + family_labels: Some(&family_labels), + name_validation_scheme: &UTF8Validation, + escaping_scheme: &NoEscaping, + }; + + encoder.encode_labels(Some(&additional_labels)).unwrap(); + assert_eq!(buffer, "{\"service.name\"=\"t1\",\"whatever\\whatever\"=\"t2\",\"t*3\"=\"t3\"}"); + } + fn parse_with_python_client(input: String) { pyo3::prepare_freethreaded_python(); From ca921094987f1175465c0a73f159baaa20296e40 Mon Sep 17 00:00:00 2001 From: Federico Torres Date: Mon, 20 Jan 2025 11:41:29 -0300 Subject: [PATCH 18/19] Additional tests Signed-off-by: Federico Torres --- src/encoding.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++ src/encoding/text.rs | 56 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/encoding.rs b/src/encoding.rs index 448cf626..e715cbf9 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -994,6 +994,62 @@ mod tests { } } + #[test] + fn label_name_is_legacy_valid() { + struct Scenario { + input: &'static str, + expected: bool, + } + + let scenarios = vec![ + Scenario { + input: "Avalid_23name", + expected: true, + }, + Scenario { + input: "_Avalid_23name", + expected: true, + }, + Scenario { + input: "1valid_23name", + expected: false, + }, + Scenario { + input: "avalid_23name", + expected: true, + }, + Scenario { + input: "Ava:lid_23name", + expected: false, + }, + Scenario { + input: "a lid_23name", + expected: false, + }, + Scenario { + input: ":leading_colon", + expected: false, + }, + Scenario { + input: "colon:in:the:middle", + expected: false, + }, + Scenario { + input: "", + expected: false, + }, + Scenario { + input: "aÅz", + expected: false, + }, + ]; + + for scenario in scenarios { + let result = is_valid_legacy_label_name(scenario.input); + assert_eq!(result, scenario.expected); + } + } + #[test] fn test_escape_name() { struct Scenario { diff --git a/src/encoding/text.rs b/src/encoding/text.rs index cc734cc6..ac6b2e5f 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -878,7 +878,7 @@ impl std::fmt::Write for LabelValueEncoder<'_> { #[cfg(test)] mod tests { use super::*; - use crate::encoding::EscapingScheme::NoEscaping; + use crate::encoding::EscapingScheme::{NoEscaping, UnderscoreEscaping}; use crate::encoding::ValidationScheme::UTF8Validation; use crate::metrics::exemplar::HistogramWithExemplars; use crate::metrics::family::Family; @@ -1028,6 +1028,29 @@ mod tests { parse_with_python_client(encoded); } + #[test] + fn encode_gauge_with_quoted_metric_name() { + let mut registry = RegistryBuilder::new() + .with_name_validation_scheme(UTF8Validation) + .with_escaping_scheme(NoEscaping) + .build(); + let gauge = Gauge::::default(); + registry.register("my.gauge\"", "My\ngau\nge\"", gauge.clone()); + + gauge.set(f32::INFINITY); + + let mut encoded = String::new(); + + encode(&mut encoded, ®istry).unwrap(); + + let expected = "# HELP \"my.gauge\"\" My\ngau\nge\".\n" + .to_owned() + + "# TYPE \"my.gauge\"\" gauge\n" + + "{\"my.gauge\"\"} inf\n" + + "# EOF\n"; + assert_eq!(expected, encoded); + } + #[test] fn encode_counter_family() { let mut registry = Registry::default(); @@ -1109,6 +1132,37 @@ mod tests { assert_eq!(expected, encoded); } + #[test] + fn escaped_encode_counter_family_with_prefix_with_label_with_quoted_metric_and_label_names() { + let mut registry = RegistryBuilder::new() + .with_name_validation_scheme(UTF8Validation) + .with_escaping_scheme(UnderscoreEscaping) + .build(); + 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 = String::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, encoded); + } + #[test] fn encode_info() { let mut registry = Registry::default(); From 1ff883d493b395a159a8dd6f2df615868bec912a Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Mon, 20 Jan 2025 16:58:08 +0000 Subject: [PATCH 19/19] perf: improve perf of encoding with new UTF-8 validation This makes four changes: 1. The `EscapingScheme` and `ValidationScheme` enums are now `Copy` since they are very small and cheap to copy. They're passed by value rather than by reference. 2. The `escape_name` function now returns a `Cow` rather than a `String` to avoid allocations in many cases. 3. `escape_name` also preallocates a buffer for the escaped name rather than starting with an empty `String` and growing it, to amortize the allocations. 4. Use `is_ascii_alphabetic` and `is_ascii_digit` to check for characters that are valid in metric and label names. Based on profiles I suspect that #2 has the highest impact but haven't split these out to see how much of a difference it makes. Signed-off-by: Ben Sully --- src/encoding.rs | 49 ++++++++++---------- src/encoding/text.rs | 105 +++++++++++++++++++++---------------------- src/registry.rs | 4 +- 3 files changed, 76 insertions(+), 82 deletions(-) diff --git a/src/encoding.rs b/src/encoding.rs index e715cbf9..4b9bdbde 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -770,7 +770,7 @@ impl ExemplarValueEncoder<'_> { /// Enum for determining how metric and label names will /// be validated. -#[derive(Debug, PartialEq, Default, Clone)] +#[derive(Debug, PartialEq, Default, Clone, Copy)] pub enum ValidationScheme { /// Setting that requires that metric and label names /// conform to the original OpenMetrics character requirements. @@ -807,9 +807,9 @@ fn is_valid_legacy_prefix(prefix: Option<&Prefix>) -> bool { fn is_quoted_metric_name( name: &str, prefix: Option<&Prefix>, - validation_scheme: &ValidationScheme, + validation_scheme: ValidationScheme, ) -> bool { - *validation_scheme == ValidationScheme::UTF8Validation + validation_scheme == ValidationScheme::UTF8Validation && (!is_valid_legacy_metric_name(name) || !is_valid_legacy_prefix(prefix)) } @@ -818,24 +818,20 @@ fn is_valid_legacy_label_name(label_name: &str) -> bool { return false; } for (i, b) in label_name.chars().enumerate() { - if !((b >= 'a' && b <= 'z') - || (b >= 'A' && b <= 'Z') - || b == '_' - || (b >= '0' && b <= '9' && i > 0)) - { + if !(b.is_ascii_alphabetic() || b == '_' || (b.is_ascii_digit() && i > 0)) { return false; } } true } -fn is_quoted_label_name(name: &str, validation_scheme: &ValidationScheme) -> bool { - *validation_scheme == ValidationScheme::UTF8Validation && !is_valid_legacy_label_name(name) +fn is_quoted_label_name(name: &str, validation_scheme: ValidationScheme) -> bool { + validation_scheme == ValidationScheme::UTF8Validation && !is_valid_legacy_label_name(name) } /// Enum for determining how metric and label names will /// be escaped. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Copy)] pub enum EscapingScheme { /// Replaces all legacy-invalid characters with underscores. #[default] @@ -863,17 +859,19 @@ impl EscapingScheme { } } -fn escape_name(name: &str, scheme: &EscapingScheme) -> String { +fn escape_name(name: &str, scheme: EscapingScheme) -> Cow<'_, str> { if name.is_empty() { - return name.to_string(); + return name.into(); } - let mut escaped = String::new(); match scheme { - EscapingScheme::NoEscaping => return name.to_string(), + EscapingScheme::NoEscaping => name.into(), + EscapingScheme::UnderscoreEscaping | EscapingScheme::ValueEncodingEscaping + if is_valid_legacy_metric_name(name) => + { + name.into() + } EscapingScheme::UnderscoreEscaping => { - if is_valid_legacy_metric_name(name) { - return name.to_string(); - } + let mut escaped = String::with_capacity(name.len()); for (i, b) in name.chars().enumerate() { if is_valid_legacy_char(b, i) { escaped.push(b); @@ -881,8 +879,10 @@ fn escape_name(name: &str, scheme: &EscapingScheme) -> String { escaped.push('_'); } } + escaped.into() } EscapingScheme::DotsEscaping => { + let mut escaped = String::with_capacity(name.len()); for (i, b) in name.chars().enumerate() { if b == '_' { escaped.push_str("__"); @@ -894,11 +894,10 @@ fn escape_name(name: &str, scheme: &EscapingScheme) -> String { escaped.push_str("__"); } } + escaped.into() } EscapingScheme::ValueEncodingEscaping => { - if is_valid_legacy_metric_name(name) { - return name.to_string(); - } + let mut escaped = String::with_capacity(name.len()); escaped.push_str("U__"); for (i, b) in name.chars().enumerate() { if b == '_' { @@ -909,9 +908,9 @@ fn escape_name(name: &str, scheme: &EscapingScheme) -> String { write!(escaped, "_{:x}_", b as i64).unwrap(); } } + escaped.into() } } - escaped } /// Returns the escaping scheme to use based on the given header. @@ -1120,13 +1119,13 @@ mod tests { ]; for scenario in scenarios { - let result = escape_name(scenario.input, &EscapingScheme::UnderscoreEscaping); + let result = escape_name(scenario.input, EscapingScheme::UnderscoreEscaping); assert_eq!(result, scenario.expected_underscores, "{}", scenario.name); - let result = escape_name(scenario.input, &EscapingScheme::DotsEscaping); + let result = escape_name(scenario.input, EscapingScheme::DotsEscaping); assert_eq!(result, scenario.expected_dots, "{}", scenario.name); - let result = escape_name(scenario.input, &EscapingScheme::ValueEncodingEscaping); + let result = escape_name(scenario.input, EscapingScheme::ValueEncodingEscaping); assert_eq!(result, scenario.expected_value, "{}", scenario.name); } } diff --git a/src/encoding/text.rs b/src/encoding/text.rs index ac6b2e5f..63d139d0 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -148,8 +148,8 @@ where registry.encode( &mut DescriptorEncoder::new( writer, - ®istry.name_validation_scheme(), - ®istry.escaping_scheme(), + registry.name_validation_scheme(), + registry.escaping_scheme(), ) .into(), ) @@ -196,8 +196,8 @@ pub(crate) struct DescriptorEncoder<'a> { writer: &'a mut dyn Write, prefix: Option<&'a Prefix>, labels: &'a [(Cow<'static, str>, Cow<'static, str>)], - name_validation_scheme: &'a ValidationScheme, - escaping_scheme: &'a EscapingScheme, + name_validation_scheme: ValidationScheme, + escaping_scheme: EscapingScheme, } impl std::fmt::Debug for DescriptorEncoder<'_> { @@ -207,11 +207,11 @@ impl std::fmt::Debug for DescriptorEncoder<'_> { } impl DescriptorEncoder<'_> { - pub(crate) fn new<'a>( - writer: &'a mut dyn Write, - name_validation_scheme: &'a ValidationScheme, - escaping_scheme: &'a EscapingScheme, - ) -> DescriptorEncoder<'a> { + pub(crate) fn new( + writer: &'_ mut dyn Write, + name_validation_scheme: ValidationScheme, + escaping_scheme: EscapingScheme, + ) -> DescriptorEncoder<'_> { DescriptorEncoder { writer, prefix: Default::default(), @@ -230,8 +230,8 @@ impl DescriptorEncoder<'_> { prefix, labels, writer: self.writer, - name_validation_scheme: &self.name_validation_scheme, - escaping_scheme: &self.escaping_scheme, + name_validation_scheme: self.name_validation_scheme, + escaping_scheme: self.escaping_scheme, } } @@ -246,14 +246,12 @@ impl DescriptorEncoder<'_> { let mut escaped_prefix: Option<&Prefix> = None; let escaped_prefix_value: Prefix; if let Some(prefix) = self.prefix { - escaped_prefix_value = Prefix::from(escape_name(prefix.as_str(), self.escaping_scheme)); + escaped_prefix_value = + Prefix::from(escape_name(prefix.as_str(), self.escaping_scheme).into_owned()); escaped_prefix = Some(&escaped_prefix_value); } - let is_quoted_metric_name = is_quoted_metric_name( - escaped_name.as_str(), - escaped_prefix, - self.name_validation_scheme, - ); + let is_quoted_metric_name = + is_quoted_metric_name(&escaped_name, escaped_prefix, self.name_validation_scheme); self.writer.write_str("# HELP ")?; if is_quoted_metric_name { self.writer.write_str("\"")?; @@ -262,7 +260,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(escaped_name.as_str())?; + self.writer.write_str(&escaped_name)?; if let Some(unit) = unit { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; @@ -282,7 +280,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(escaped_name.as_str())?; + self.writer.write_str(&escaped_name)?; if let Some(unit) = unit { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; @@ -303,7 +301,7 @@ impl DescriptorEncoder<'_> { self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(escaped_name.as_str())?; + self.writer.write_str(&escaped_name)?; self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; if is_quoted_metric_name { @@ -343,8 +341,8 @@ pub(crate) struct MetricEncoder<'a> { unit: Option<&'a Unit>, const_labels: &'a [(Cow<'static, str>, Cow<'static, str>)], family_labels: Option<&'a dyn super::EncodeLabelSet>, - name_validation_scheme: &'a ValidationScheme, - escaping_scheme: &'a EscapingScheme, + name_validation_scheme: ValidationScheme, + escaping_scheme: EscapingScheme, } impl std::fmt::Debug for MetricEncoder<'_> { @@ -534,14 +532,12 @@ impl MetricEncoder<'_> { let mut escaped_prefix: Option<&Prefix> = None; let escaped_prefix_value: Prefix; if let Some(prefix) = self.prefix { - escaped_prefix_value = Prefix::from(escape_name(prefix.as_str(), self.escaping_scheme)); + escaped_prefix_value = + Prefix::from(escape_name(prefix.as_str(), self.escaping_scheme).into_owned()); escaped_prefix = Some(&escaped_prefix_value); } - let is_quoted_metric_name = is_quoted_metric_name( - escaped_name.as_str(), - escaped_prefix, - self.name_validation_scheme, - ); + let is_quoted_metric_name = + is_quoted_metric_name(&escaped_name, escaped_prefix, self.name_validation_scheme); if is_quoted_metric_name { self.writer.write_str("{")?; self.writer.write_str("\"")?; @@ -550,7 +546,7 @@ impl MetricEncoder<'_> { self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(escaped_name.as_str())?; + self.writer.write_str(&escaped_name)?; if let Some(unit) = self.unit { self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; @@ -576,14 +572,12 @@ impl MetricEncoder<'_> { let mut escaped_prefix: Option<&Prefix> = None; let escaped_prefix_value: Prefix; if let Some(prefix) = self.prefix { - escaped_prefix_value = Prefix::from(escape_name(prefix.as_str(), self.escaping_scheme)); + escaped_prefix_value = + Prefix::from(escape_name(prefix.as_str(), self.escaping_scheme).into_owned()); escaped_prefix = Some(&escaped_prefix_value); } - let is_quoted_metric_name = is_quoted_metric_name( - escaped_name.as_str(), - escaped_prefix, - self.name_validation_scheme, - ); + let is_quoted_metric_name = + is_quoted_metric_name(&escaped_name, escaped_prefix, self.name_validation_scheme); if self.const_labels.is_empty() && additional_labels.is_none() && self.family_labels.is_none() @@ -750,8 +744,8 @@ impl ExemplarValueEncoder<'_> { pub(crate) struct LabelSetEncoder<'a> { writer: &'a mut dyn Write, first: bool, - name_validation_scheme: &'a ValidationScheme, - escaping_scheme: &'a EscapingScheme, + name_validation_scheme: ValidationScheme, + escaping_scheme: EscapingScheme, } impl std::fmt::Debug for LabelSetEncoder<'_> { @@ -765,8 +759,8 @@ impl std::fmt::Debug for LabelSetEncoder<'_> { impl<'a> LabelSetEncoder<'a> { fn new( writer: &'a mut dyn Write, - name_validation_scheme: &'a ValidationScheme, - escaping_scheme: &'a EscapingScheme, + name_validation_scheme: ValidationScheme, + escaping_scheme: EscapingScheme, ) -> Self { Self { writer, @@ -791,8 +785,8 @@ impl<'a> LabelSetEncoder<'a> { pub(crate) struct LabelEncoder<'a> { writer: &'a mut dyn Write, first: bool, - name_validation_scheme: &'a ValidationScheme, - escaping_scheme: &'a EscapingScheme, + name_validation_scheme: ValidationScheme, + escaping_scheme: EscapingScheme, } impl std::fmt::Debug for LabelEncoder<'_> { @@ -818,8 +812,8 @@ impl LabelEncoder<'_> { pub(crate) struct LabelKeyEncoder<'a> { writer: &'a mut dyn Write, - name_validation_scheme: &'a ValidationScheme, - escaping_scheme: &'a EscapingScheme, + name_validation_scheme: ValidationScheme, + escaping_scheme: EscapingScheme, } impl std::fmt::Debug for LabelKeyEncoder<'_> { @@ -840,12 +834,11 @@ impl<'a> LabelKeyEncoder<'a> { impl std::fmt::Write for LabelKeyEncoder<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { let escaped_name = escape_name(s, self.escaping_scheme); - let is_quoted_label_name = - is_quoted_label_name(escaped_name.as_str(), self.name_validation_scheme); + let is_quoted_label_name = is_quoted_label_name(&escaped_name, self.name_validation_scheme); if is_quoted_label_name { self.writer.write_str("\"")?; } - self.writer.write_str(escaped_name.as_str())?; + self.writer.write_str(&escaped_name)?; if is_quoted_label_name { self.writer.write_str("\"")?; } @@ -1043,8 +1036,7 @@ mod tests { encode(&mut encoded, ®istry).unwrap(); - let expected = "# HELP \"my.gauge\"\" My\ngau\nge\".\n" - .to_owned() + let expected = "# HELP \"my.gauge\"\" My\ngau\nge\".\n".to_owned() + "# TYPE \"my.gauge\"\" gauge\n" + "{\"my.gauge\"\"} inf\n" + "# EOF\n"; @@ -1646,8 +1638,8 @@ mod tests { unit: None, const_labels: &[], family_labels: None, - name_validation_scheme: &UTF8Validation, - escaping_scheme: &NoEscaping, + name_validation_scheme: UTF8Validation, + escaping_scheme: NoEscaping, }; encoder.encode_labels::(None).unwrap(); @@ -1667,8 +1659,8 @@ mod tests { unit: None, const_labels: &const_labels, family_labels: Some(&family_labels), - name_validation_scheme: &UTF8Validation, - escaping_scheme: &NoEscaping, + name_validation_scheme: UTF8Validation, + escaping_scheme: NoEscaping, }; encoder.encode_labels(Some(&additional_labels)).unwrap(); @@ -1688,12 +1680,15 @@ mod tests { unit: None, const_labels: &const_labels, family_labels: Some(&family_labels), - name_validation_scheme: &UTF8Validation, - escaping_scheme: &NoEscaping, + name_validation_scheme: UTF8Validation, + escaping_scheme: NoEscaping, }; encoder.encode_labels(Some(&additional_labels)).unwrap(); - assert_eq!(buffer, "{\"service.name\"=\"t1\",\"whatever\\whatever\"=\"t2\",\"t*3\"=\"t3\"}"); + assert_eq!( + buffer, + "{\"service.name\"=\"t1\",\"whatever\\whatever\"=\"t2\",\"t*3\"=\"t3\"}" + ); } fn parse_with_python_client(input: String) { diff --git a/src/registry.rs b/src/registry.rs index 2d082984..f5899f2c 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -101,12 +101,12 @@ impl Registry { /// Returns the given Registry's name validation scheme. pub fn name_validation_scheme(&self) -> ValidationScheme { - self.name_validation_scheme.clone() + self.name_validation_scheme } /// Returns the given Registry's escaping scheme. pub fn escaping_scheme(&self) -> EscapingScheme { - self.escaping_scheme.clone() + self.escaping_scheme } /// Sets the escaping scheme for the [`RegistryBuilder`].