diff --git a/examples/axum-utf-8.rs b/examples/axum-utf-8.rs new file mode 100644 index 0000000..0c6a1e4 --- /dev/null +++ b/examples/axum-utf-8.rs @@ -0,0 +1,95 @@ +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::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; + +#[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 9e2acac..85b13dc 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -767,3 +767,171 @@ impl ExemplarValueEncoder<'_> { for_both_mut!(self, ExemplarValueEncoderInner, e, e.encode(v)) } } + +/// 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, +} + +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 +} + +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) +} + +/// 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, +} + +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(); + } + 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 +} + +/// Returns the escaping scheme to use based on the given header. +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; + } + if header.contains("values") { + return EscapingScheme::ValueEncodingEscaping; + } + 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 8f3c63d..605b21d 100644 --- a/src/encoding/text.rs +++ b/src/encoding/text.rs @@ -37,7 +37,10 @@ //! assert_eq!(expected_msg, buffer); //! ``` -use crate::encoding::{EncodeExemplarValue, EncodeLabelSet, 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}; @@ -142,7 +145,14 @@ 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(), + ®istry.escaping_scheme(), + ) + .into(), + ) } /// Encode the EOF marker into the provided [`Write`]r using the OpenMetrics @@ -186,6 +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, } impl std::fmt::Debug for DescriptorEncoder<'_> { @@ -195,11 +207,17 @@ impl std::fmt::Debug for DescriptorEncoder<'_> { } 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, + escaping_scheme: &'a EscapingScheme, + ) -> DescriptorEncoder<'a> { DescriptorEncoder { writer, prefix: Default::default(), labels: Default::default(), + name_validation_scheme, + escaping_scheme, } } @@ -212,6 +230,8 @@ impl DescriptorEncoder<'_> { prefix, labels, writer: self.writer, + name_validation_scheme: &self.name_validation_scheme, + escaping_scheme: &self.escaping_scheme, } } @@ -222,43 +242,73 @@ impl DescriptorEncoder<'_> { unit: Option<&'s Unit>, metric_type: MetricType, ) -> Result, std::fmt::Error> { - self.writer.write_str("# HELP ")?; + 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 { + self.writer.write_str("\"")?; + } + if let Some(prefix) = escaped_prefix { self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(name)?; + 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 { + self.writer.write_str("\"")?; + } self.writer.write_str(" ")?; self.writer.write_str(help)?; self.writer.write_str("\n")?; self.writer.write_str("# TYPE ")?; - if let Some(prefix) = self.prefix { + if is_quoted_metric_name { + self.writer.write_str("\"")?; + } + if let Some(prefix) = escaped_prefix { self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(name)?; + 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 { + 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 let Some(prefix) = self.prefix { + if is_quoted_metric_name { + self.writer.write_str("\"")?; + } + if let Some(prefix) = escaped_prefix { self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(name)?; + self.writer.write_str(escaped_name.as_str())?; self.writer.write_str("_")?; self.writer.write_str(unit.as_str())?; + if is_quoted_metric_name { + self.writer.write_str("\"")?; + } self.writer.write_str(" ")?; self.writer.write_str(unit.as_str())?; self.writer.write_str("\n")?; @@ -271,6 +321,8 @@ impl DescriptorEncoder<'_> { unit, const_labels: self.labels, family_labels: None, + name_validation_scheme: self.name_validation_scheme, + escaping_scheme: self.escaping_scheme, }) } } @@ -291,13 +343,22 @@ 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, } impl std::fmt::Debug for MetricEncoder<'_> { 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, + self.escaping_scheme, + ) + .into(), + )?; } f.debug_struct("Encoder") @@ -320,9 +381,7 @@ impl MetricEncoder<'_> { v: &CounterValue, exemplar: Option<&Exemplar>, ) -> Result<(), std::fmt::Error> { - self.write_prefix_name_unit()?; - - self.write_suffix("total")?; + self.write_prefix_name_unit_suffix(Option::from("total"))?; self.encode_labels::(None)?; @@ -346,7 +405,7 @@ impl MetricEncoder<'_> { &mut self, v: &GaugeValue, ) -> Result<(), std::fmt::Error> { - self.write_prefix_name_unit()?; + self.write_prefix_name_unit_suffix(None)?; self.encode_labels::(None)?; @@ -363,9 +422,7 @@ impl MetricEncoder<'_> { } pub fn encode_info(&mut self, label_set: &S) -> Result<(), std::fmt::Error> { - self.write_prefix_name_unit()?; - - self.write_suffix("info")?; + self.write_prefix_name_unit_suffix(Option::from("info"))?; self.encode_labels(Some(label_set))?; @@ -392,6 +449,8 @@ impl MetricEncoder<'_> { unit: self.unit, const_labels: self.const_labels, family_labels: Some(label_set), + name_validation_scheme: self.name_validation_scheme, + escaping_scheme: self.escaping_scheme, }) } @@ -402,15 +461,13 @@ impl MetricEncoder<'_> { 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.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.encode_labels::(None)?; self.writer.write_str(" ")?; self.writer.write_str(itoa::Buffer::new().format(count))?; @@ -420,8 +477,7 @@ impl MetricEncoder<'_> { 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"))?; if *upper_bound == f64::MAX { self.encode_labels(Some(&[("le", "+Inf")]))?; @@ -449,9 +505,14 @@ impl MetricEncoder<'_> { exemplar: &Exemplar, ) -> Result<(), std::fmt::Error> { self.writer.write_str(" # {")?; - exemplar - .label_set - .encode(LabelSetEncoder::new(self.writer).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 { @@ -465,23 +526,42 @@ impl MetricEncoder<'_> { 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> { + 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("\"")?; + } + if let Some(prefix) = escaped_prefix { self.writer.write_str(prefix.as_str())?; self.writer.write_str("_")?; } - self.writer.write_str(self.name)?; + 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())?; } - - Ok(()) - } - - fn write_suffix(&mut self, suffix: &'static str) -> Result<(), std::fmt::Error> { - self.writer.write_str("_")?; - self.writer.write_str(suffix)?; + if let Some(suffix) = suffix { + self.writer.write_str("_")?; + self.writer.write_str(suffix)?; + } + if is_quoted_metric_name { + self.writer.write_str("\"")?; + } Ok(()) } @@ -492,24 +572,56 @@ impl MetricEncoder<'_> { &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.writer.write_str("}")?; + } return Ok(()); } - self.writer.write_str("{")?; + if is_quoted_metric_name { + self.writer.write_str(",")?; + } else { + self.writer.write_str("{")?; + } - self.const_labels - .encode(LabelSetEncoder::new(self.writer).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).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 @@ -539,9 +651,23 @@ impl MetricEncoder<'_> { writer: self.writer, should_prepend: true, }; - labels.encode(LabelSetEncoder::new(&mut writer).into())?; + labels.encode( + LabelSetEncoder::new( + &mut writer, + self.name_validation_scheme, + self.escaping_scheme, + ) + .into(), + )?; } else { - labels.encode(LabelSetEncoder::new(self.writer).into())?; + labels.encode( + LabelSetEncoder::new( + self.writer, + self.name_validation_scheme, + self.escaping_scheme, + ) + .into(), + )?; }; } @@ -624,6 +750,8 @@ impl ExemplarValueEncoder<'_> { pub(crate) struct LabelSetEncoder<'a> { writer: &'a mut dyn Write, first: bool, + name_validation_scheme: &'a ValidationScheme, + escaping_scheme: &'a EscapingScheme, } impl std::fmt::Debug for LabelSetEncoder<'_> { @@ -635,10 +763,16 @@ impl std::fmt::Debug for LabelSetEncoder<'_> { } impl<'a> LabelSetEncoder<'a> { - fn new(writer: &'a mut dyn Write) -> 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, } } @@ -648,6 +782,8 @@ impl<'a> LabelSetEncoder<'a> { LabelEncoder { writer: self.writer, first, + name_validation_scheme: self.name_validation_scheme, + escaping_scheme: self.escaping_scheme, } } } @@ -655,6 +791,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, } impl std::fmt::Debug for LabelEncoder<'_> { @@ -672,12 +810,16 @@ impl LabelEncoder<'_> { } Ok(LabelKeyEncoder { writer: self.writer, + name_validation_scheme: self.name_validation_scheme, + escaping_scheme: self.escaping_scheme, }) } } pub(crate) struct LabelKeyEncoder<'a> { writer: &'a mut dyn Write, + name_validation_scheme: &'a ValidationScheme, + escaping_scheme: &'a EscapingScheme, } impl std::fmt::Debug for LabelKeyEncoder<'_> { @@ -697,7 +839,17 @@ impl<'a> LabelKeyEncoder<'a> { impl std::fmt::Write for LabelKeyEncoder<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { - self.writer.write_str(s) + 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(escaped_name.as_str())?; + if is_quoted_label_name { + self.writer.write_str("\"")?; + } + Ok(()) } } @@ -726,12 +878,15 @@ impl std::fmt::Write for LabelValueEncoder<'_> { #[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; @@ -773,6 +928,26 @@ mod tests { parse_with_python_client(encoded); } + #[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 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); + } + #[test] fn encode_counter_with_exemplar() { let mut registry = Registry::default(); @@ -802,6 +977,36 @@ mod tests { parse_with_python_client(encoded); } + #[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 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); + } + #[test] fn encode_gauge() { let mut registry = Registry::default(); @@ -873,6 +1078,37 @@ mod tests { parse_with_python_client(encoded); } + #[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 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(); @@ -891,6 +1127,25 @@ mod tests { parse_with_python_client(encoded); } + #[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 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); + } + #[test] fn encode_histogram() { let mut registry = Registry::default(); @@ -981,6 +1236,38 @@ mod tests { parse_with_python_client(encoded); } + #[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 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); + } + #[test] fn sub_registry_with_prefix_and_label() { let top_level_metric_name = "my_top_level_metric"; @@ -1055,6 +1342,81 @@ mod tests { parse_with_python_client(encoded); } + #[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 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); + } + #[test] fn sub_registry_collector() { use crate::encoding::EncodeMetric; @@ -1114,6 +1476,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(()) + } + } + + 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"); + 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); + } + #[test] fn encode_registry_eof() { let mut orders_registry = Registry::default(); diff --git a/src/registry.rs b/src/registry.rs index c7dec3b..2d08298 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, ValidationScheme}; /// A metric registry. /// @@ -64,6 +64,8 @@ pub struct Registry { metrics: Vec<(Descriptor, Box)>, collectors: Vec>, sub_registries: Vec, + name_validation_scheme: ValidationScheme, + escaping_scheme: EscapingScheme, } impl Registry { @@ -97,6 +99,21 @@ impl Registry { } } + /// Returns the given Registry's name validation scheme. + pub fn name_validation_scheme(&self) -> ValidationScheme { + self.name_validation_scheme.clone() + } + + /// Returns the given Registry's escaping scheme. + 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 @@ -313,6 +330,79 @@ 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, + labels: Vec<(Cow<'static, str>, Cow<'static, str>)>, + name_validation_scheme: ValidationScheme, + escaping_scheme: EscapingScheme, +} + +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>)>, + ) -> Self { + self.labels = labels.into_iter().collect(); + 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, + 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);