Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions foundations-metrics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ serde = { workspace = true, features = ["derive"] }
ryu = "1.0.23"
parking_lot = { workspace = true }
prost = { workspace = true }
prost-types = { workspace = true }

[lints]
workspace = true
6 changes: 6 additions & 0 deletions foundations-metrics/src/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,10 +447,16 @@ mod tests {
exemplars: vec![
Exemplar {
label: vec![label("bad name", "bad")],
timestamp: Some(Default::default()),
..Default::default()
},
Exemplar {
label: vec![label("trace_id", "missing_timestamp")],
..Default::default()
},
Exemplar {
label: vec![label("trace_id", "good")],
timestamp: Some(Default::default()),
..Default::default()
},
],
Expand Down
6 changes: 6 additions & 0 deletions foundations-metrics/src/encoding/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,16 @@ mod tests {
exemplars: vec![
Exemplar {
label: vec![label("bad#name", "nonstandard")],
timestamp: Some(Default::default()),
..Default::default()
},
Exemplar {
label: vec![label("trace_id", "missing_timestamp")],
..Default::default()
},
Exemplar {
label: vec![label("trace_id", "good")],
timestamp: Some(Default::default()),
..Default::default()
},
],
Expand Down
8 changes: 6 additions & 2 deletions foundations-metrics/src/encoding/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,13 @@ fn write_sample(
write_float(output, timestamp_ms as f64 / 1_000.0);
}

if let Some(exemplar) = exemplar.filter(|exemplar| !exemplar.label.is_empty()) {
if let Some(exemplar) = exemplar {
output.push_str(" # ");
write_labels(output, &exemplar.label, None);
if exemplar.label.is_empty() {
output.push_str("{}");
} else {
write_labels(output, &exemplar.label, None);
}
output.push(' ');
write_float(output, exemplar.value.unwrap_or_default());

Expand Down
118 changes: 111 additions & 7 deletions foundations-metrics/src/labels/serializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::fmt;

use foundations_metrics_registry::proto::LabelPair;
use serde::Serialize;
use serde::ser::{Impossible, SerializeStruct, Serializer};
use serde::ser::{Impossible, SerializeSeq, SerializeStruct, SerializeTuple, Serializer};

use super::LabelError;
use crate::validation::{NAME_REQUIREMENT, is_valid_name};
Expand All @@ -14,8 +14,8 @@ pub(super) struct LabelSetSerializer;
impl Serializer for LabelSetSerializer {
type Ok = Vec<LabelPair>;
type Error = LabelError;
type SerializeSeq = Impossible<Self::Ok, Self::Error>;
type SerializeTuple = Impossible<Self::Ok, Self::Error>;
type SerializeSeq = LabelSequenceSerializer;
type SerializeTuple = LabelTupleSerializer;
type SerializeTupleStruct = Impossible<Self::Ok, Self::Error>;
type SerializeTupleVariant = Impossible<Self::Ok, Self::Error>;
type SerializeMap = Impossible<Self::Ok, Self::Error>;
Expand Down Expand Up @@ -148,12 +148,21 @@ impl Serializer for LabelSetSerializer {
Err(invalid_label_set())
}

fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
Err(invalid_label_set())
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
Ok(LabelSequenceSerializer {
labels: Vec::with_capacity(len.unwrap_or_default()),
})
}

fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
Err(invalid_label_set())
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> {
if len != 2 {
return Err(LabelError::new("label pairs must contain a name and value"));
}

Ok(LabelTupleSerializer {
name: None,
value: None,
})
}

fn serialize_tuple_struct(
Expand Down Expand Up @@ -193,6 +202,77 @@ impl Serializer for LabelSetSerializer {
}
}

pub(super) struct LabelSequenceSerializer {
labels: Vec<LabelPair>,
}

impl SerializeSeq for LabelSequenceSerializer {
type Ok = Vec<LabelPair>;
type Error = LabelError;

fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: Serialize + ?Sized,
{
let mut labels = value.serialize(LabelSetSerializer)?;
if labels.len() != 1 {
return Err(LabelError::new(
"label sequences must contain name-value pairs",
));
}
self.labels
.push(labels.pop().expect("one label was encoded"));
Ok(())
}

fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(self.labels)
}
}

pub(super) struct LabelTupleSerializer {
name: Option<String>,
value: Option<String>,
}

impl SerializeTuple for LabelTupleSerializer {
type Ok = Vec<LabelPair>;
type Error = LabelError;

fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: Serialize + ?Sized,
{
if self.name.is_none() {
let name = value.serialize(LabelValueSerializer)?;
validate_label_name(&name)?;
self.name = Some(name);
} else if self.value.is_none() {
self.value = Some(value.serialize(LabelValueSerializer)?);
} else {
return Err(LabelError::new(
"label pairs must contain exactly one name and value",
));
}

Ok(())
}

fn end(self) -> Result<Self::Ok, Self::Error> {
let name = self
.name
.ok_or_else(|| LabelError::new("label pair is missing its name"))?;
let value = self
.value
.ok_or_else(|| LabelError::new("label pair is missing its value"))?;

Ok(vec![LabelPair {
name: Some(name),
value: Some(value),
}])
}
}

// Adapted from prometools' `serde::top::StructSerializer`
// (https://github.com/nox/prometools, licensed MIT OR Apache-2.0).
pub(super) struct LabelPairSerializer {
Expand Down Expand Up @@ -468,6 +548,30 @@ mod tests {
);
}

#[test]
fn serializes_legacy_name_value_sequences() {
let pairs = to_label_pairs(&vec![
("trace_id", "abc"),
("span_id", "def"),
("trace:id", "ghi"),
])
.unwrap();
let values: Vec<_> = pairs
.iter()
.map(|pair| {
(
pair.name.as_deref().unwrap(),
pair.value.as_deref().unwrap(),
)
})
.collect();

assert_eq!(
values,
[("trace_id", "abc"), ("span_id", "def"), ("trace:id", "ghi"),]
);
}

#[test]
fn unit_is_an_empty_label_set() {
assert!(to_label_pairs(&()).unwrap().is_empty());
Expand Down
7 changes: 4 additions & 3 deletions foundations-metrics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ pub use foundations_metrics_registry::{
};
pub use labels::{LabelError, to_label_pairs};
pub use metrics::{
Counter, CounterAtomic, Family, FamilyMetricGuard, Gauge, GaugeAtomic, GaugeGuard, Histogram,
HistogramBuilder, HistogramSnapshot, HistogramTimer, MetricConstructor, NativeHistogram,
NativeHistogramBuilder, RangeGauge, TimeHistogram,
Counter, CounterAtomic, CounterWithExemplar, Exemplar, Family, FamilyMetricGuard, Gauge,
GaugeAtomic, GaugeGuard, Histogram, HistogramBuilder, HistogramSnapshot, HistogramTimer,
HistogramWithExemplars, MetricConstructor, NativeHistogram, NativeHistogramBuilder,
NativeHistogramWithExemplars, RangeGauge, TimeHistogram,
};
pub use registered::NamedMetric;
2 changes: 1 addition & 1 deletion foundations-metrics/src/metrics/counter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ impl<N, A: Default> Default for Counter<N, A> {
///
/// The name/help are left empty here; they are populated at registration and
/// encode time.
fn encode_counter(value: f64) -> Vec<MetricFamily> {
pub(super) fn encode_counter(value: f64) -> Vec<MetricFamily> {
vec![MetricFamily {
name: Some(String::new()),
help: None,
Expand Down
Loading
Loading