Skip to content

Commit ad05f0f

Browse files
authored
fix(encoding): correctly handle empty family labels (prometheus#224)
Signed-off-by: Tyler Levine <[email protected]> Signed-off-by: Max Inden <[email protected]>
1 parent 0c3e89e commit ad05f0f

File tree

6 files changed

+79
-17
lines changed

6 files changed

+79
-17
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
[PR 216]: https://github.com/prometheus/client_rust/pull/216
2929
[PR 217]: https://github.com/prometheus/client_rust/pull/217
3030

31+
### Fixed
32+
33+
- Don't prepend `,` when encoding empty family label set.
34+
See [PR 175].
35+
36+
[PR 175]: https://github.com/prometheus/client_rust/pull/175
37+
3138
## [0.22.3]
3239

3340
### Added

examples/custom-metric.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use prometheus_client::encoding::{text::encode, EncodeMetric, MetricEncoder};
1+
use prometheus_client::encoding::{text::encode, EncodeMetric, MetricEncoder, NoLabelSet};
22
use prometheus_client::metrics::MetricType;
33
use prometheus_client::registry::Registry;
44

@@ -20,7 +20,7 @@ impl EncodeMetric for MyCustomMetric {
2020
// E.g. every CPU cycle spend in this method delays the response send to
2121
// the Prometheus server.
2222

23-
encoder.encode_counter::<(), _, u64>(&rand::random::<u64>(), None)
23+
encoder.encode_counter::<NoLabelSet, _, u64>(&rand::random::<u64>(), None)
2424
}
2525

2626
fn metric_type(&self) -> prometheus_client::metrics::MetricType {

src/encoding.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ pub trait EncodeLabel {
247247
#[derive(Debug)]
248248
pub struct LabelEncoder<'a>(LabelEncoderInner<'a>);
249249

250+
/// Uninhabited type to represent the lack of a label set for a metric
251+
#[derive(Debug)]
252+
pub enum NoLabelSet {}
253+
250254
#[derive(Debug)]
251255
enum LabelEncoderInner<'a> {
252256
Text(text::LabelEncoder<'a>),
@@ -352,7 +356,7 @@ impl<T: EncodeLabel> EncodeLabelSet for Vec<T> {
352356
}
353357
}
354358

355-
impl EncodeLabelSet for () {
359+
impl EncodeLabelSet for NoLabelSet {
356360
fn encode(&self, _encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> {
357361
Ok(())
358362
}

src/encoding/text.rs

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
//! assert_eq!(expected_msg, buffer);
3838
//! ```
3939
40-
use crate::encoding::{EncodeExemplarValue, EncodeLabelSet};
40+
use crate::encoding::{EncodeExemplarValue, EncodeLabelSet, NoLabelSet};
4141
use crate::metrics::exemplar::Exemplar;
4242
use crate::metrics::MetricType;
4343
use crate::registry::{Prefix, Registry, Unit};
@@ -324,7 +324,7 @@ impl<'a> MetricEncoder<'a> {
324324

325325
self.write_suffix("total")?;
326326

327-
self.encode_labels::<()>(None)?;
327+
self.encode_labels::<NoLabelSet>(None)?;
328328

329329
v.encode(
330330
&mut CounterValueEncoder {
@@ -348,7 +348,7 @@ impl<'a> MetricEncoder<'a> {
348348
) -> Result<(), std::fmt::Error> {
349349
self.write_prefix_name_unit()?;
350350

351-
self.encode_labels::<()>(None)?;
351+
self.encode_labels::<NoLabelSet>(None)?;
352352

353353
v.encode(
354354
&mut GaugeValueEncoder {
@@ -404,14 +404,14 @@ impl<'a> MetricEncoder<'a> {
404404
) -> Result<(), std::fmt::Error> {
405405
self.write_prefix_name_unit()?;
406406
self.write_suffix("sum")?;
407-
self.encode_labels::<()>(None)?;
407+
self.encode_labels::<NoLabelSet>(None)?;
408408
self.writer.write_str(" ")?;
409409
self.writer.write_str(dtoa::Buffer::new().format(sum))?;
410410
self.newline()?;
411411

412412
self.write_prefix_name_unit()?;
413413
self.write_suffix("count")?;
414-
self.encode_labels::<()>(None)?;
414+
self.encode_labels::<NoLabelSet>(None)?;
415415
self.writer.write_str(" ")?;
416416
self.writer.write_str(itoa::Buffer::new().format(count))?;
417417
self.newline()?;
@@ -512,12 +512,37 @@ impl<'a> MetricEncoder<'a> {
512512
additional_labels.encode(LabelSetEncoder::new(self.writer).into())?;
513513
}
514514

515-
if let Some(labels) = &self.family_labels {
516-
if !self.const_labels.is_empty() || additional_labels.is_some() {
517-
self.writer.write_str(",")?;
515+
/// Writer impl which prepends a comma on the first call to write output to the wrapped writer
516+
struct CommaPrependingWriter<'a> {
517+
writer: &'a mut dyn Write,
518+
should_prepend: bool,
519+
}
520+
521+
impl Write for CommaPrependingWriter<'_> {
522+
fn write_str(&mut self, s: &str) -> std::fmt::Result {
523+
if self.should_prepend {
524+
self.writer.write_char(',')?;
525+
self.should_prepend = false;
526+
}
527+
self.writer.write_str(s)
518528
}
529+
}
519530

520-
labels.encode(LabelSetEncoder::new(self.writer).into())?;
531+
if let Some(labels) = self.family_labels {
532+
// if const labels or additional labels have been written, a comma must be prepended before writing the family labels.
533+
// However, it could be the case that the family labels are `Some` and yet empty, so the comma should _only_
534+
// be prepended if one of the `Write` methods are actually called when attempting to write the family labels.
535+
// Therefore, wrap the writer on `Self` with a CommaPrependingWriter if other labels have been written and
536+
// there may be a need to prepend an extra comma before writing additional labels.
537+
if !self.const_labels.is_empty() || additional_labels.is_some() {
538+
let mut writer = CommaPrependingWriter {
539+
writer: self.writer,
540+
should_prepend: true,
541+
};
542+
labels.encode(LabelSetEncoder::new(&mut writer).into())?;
543+
} else {
544+
labels.encode(LabelSetEncoder::new(self.writer).into())?;
545+
};
521546
}
522547

523548
self.writer.write_str("}")?;
@@ -709,6 +734,7 @@ mod tests {
709734
use crate::metrics::{counter::Counter, exemplar::CounterWithExemplar};
710735
use pyo3::{prelude::*, types::PyModule};
711736
use std::borrow::Cow;
737+
use std::fmt::Error;
712738
use std::sync::atomic::{AtomicI32, AtomicU32};
713739

714740
#[test]
@@ -899,6 +925,31 @@ mod tests {
899925
parse_with_python_client(encoded);
900926
}
901927

928+
#[test]
929+
fn encode_histogram_family_with_empty_struct_family_labels() {
930+
let mut registry = Registry::default();
931+
let family =
932+
Family::new_with_constructor(|| Histogram::new(exponential_buckets(1.0, 2.0, 10)));
933+
registry.register("my_histogram", "My histogram", family.clone());
934+
935+
#[derive(Eq, PartialEq, Hash, Debug, Clone)]
936+
struct EmptyLabels {}
937+
938+
impl EncodeLabelSet for EmptyLabels {
939+
fn encode(&self, _encoder: crate::encoding::LabelSetEncoder) -> Result<(), Error> {
940+
Ok(())
941+
}
942+
}
943+
944+
family.get_or_create(&EmptyLabels {}).observe(1.0);
945+
946+
let mut encoded = String::new();
947+
948+
encode(&mut encoded, &registry).unwrap();
949+
950+
parse_with_python_client(encoded);
951+
}
952+
902953
#[test]
903954
fn encode_histogram_with_exemplars() {
904955
let mut registry = Registry::default();

src/metrics/counter.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//!
33
//! See [`Counter`] for details.
44
5-
use crate::encoding::{EncodeMetric, MetricEncoder};
5+
use crate::encoding::{EncodeMetric, MetricEncoder, NoLabelSet};
66

77
use super::{MetricType, TypedMetric};
88
use std::marker::PhantomData;
@@ -204,7 +204,7 @@ where
204204
A: Atomic<N>,
205205
{
206206
fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> {
207-
encoder.encode_counter::<(), _, u64>(&self.get(), None)
207+
encoder.encode_counter::<NoLabelSet, _, u64>(&self.get(), None)
208208
}
209209

210210
fn metric_type(&self) -> MetricType {
@@ -236,7 +236,7 @@ where
236236
N: crate::encoding::EncodeCounterValue,
237237
{
238238
fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> {
239-
encoder.encode_counter::<(), _, u64>(&self.value, None)
239+
encoder.encode_counter::<NoLabelSet, _, u64>(&self.value, None)
240240
}
241241

242242
fn metric_type(&self) -> MetricType {

src/metrics/histogram.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//!
33
//! See [`Histogram`] for details.
44
5-
use crate::encoding::{EncodeMetric, MetricEncoder};
5+
use crate::encoding::{EncodeMetric, MetricEncoder, NoLabelSet};
66

77
use super::{MetricType, TypedMetric};
88
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
@@ -133,7 +133,7 @@ pub fn linear_buckets(start: f64, width: f64, length: u16) -> impl Iterator<Item
133133
impl EncodeMetric for Histogram {
134134
fn encode(&self, mut encoder: MetricEncoder) -> Result<(), std::fmt::Error> {
135135
let (sum, count, buckets) = self.get();
136-
encoder.encode_histogram::<()>(sum, count, &buckets, None)
136+
encoder.encode_histogram::<NoLabelSet>(sum, count, &buckets, None)
137137
}
138138

139139
fn metric_type(&self) -> MetricType {

0 commit comments

Comments
 (0)