Skip to content

Commit 2f5437b

Browse files
authored
Merge branch 'master' into add-usize-isize-encode-impls
Signed-off-by: Max Inden <mail@max-inden.de>
2 parents ca8decb + 3942d06 commit 2f5437b

9 files changed

Lines changed: 209 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [0.24.1]
88

9+
### Added
10+
11+
- `EncodeGaugeValue` is implemented for `usize` and `isize`, and
12+
`EncodeCounterValue` is implemented for `usize`. See [PR 282].
13+
914
### Fixed
1015

1116
- `EncodeGaugeValue`, `EncodeCounterValue` and `EncodeExemplarValue` now use
1217
fewer `as` casts in their implementation. This caught an issue where
1318
`EncodeGaugeValue` would not error when encoding some `u64`s that don't fit
1419
in a `i64`. See [PR 281].
15-
16-
- `EncodeGaugeValue` is implemented for `usize` and `isize`, and
17-
`EncodeCounterValue` is implemented for `usize`. See [PR 282].
18-
20+
- Filter out empty metric families, to match the go client. See [PR 279].
21+
- `Histogram` now exposes `count()` and `sum()` methods when the `test-util`
22+
feature is enabled. See [PR 242].
23+
- `Family` now exposes a `contains()` method when the `test-util` feature
24+
is enabled. See [PR 245].
25+
- `Family` now exposes `len()` and `is_empty()` methods when the
26+
`test-util` feature is enabled. See [PR 246].
27+
28+
[PR 279]: https://github.com/prometheus/client_rust/pull/279
29+
[PR 281]: https://github.com/prometheus/client_rust/pull/281
30+
[PR 242]: https://github.com/prometheus/client_rust/pull/242
31+
[PR 245]: https://github.com/prometheus/client_rust/pull/245
32+
[PR 246]: https://github.com/prometheus/client_rust/pull/246
1933
[PR 281]: https://github.com/prometheus/client_rust/pull/281
2034
[PR 282]: https://github.com/prometheus/client_rust/pull/282
2135

Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ documentation = "https://docs.rs/prometheus-client"
1414
default = []
1515
protobuf = ["dep:prost", "dep:prost-types", "dep:prost-build"]
1616

17+
# This feature provides additional APIs for testing downstream code using
18+
# `prometheus-client`.
19+
#
20+
# Note: Interfaces gated by this feature flag are not subject to stability
21+
# guarantees and may be changed or removed in patch releases.
22+
test-util = []
23+
1724
[workspace]
1825
members = ["derive-encode"]
1926

@@ -31,7 +38,7 @@ axum = "0.7"
3138
criterion = "0.5"
3239
futures = "0.3"
3340
http-types = "2"
34-
pyo3 = "0.22"
41+
pyo3 = "0.27"
3542
quickcheck = "1"
3643
rand = "0.8.4"
3744
tide = "0.16"

src/encoding.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ pub trait EncodeMetric {
5050
// One can not use [`TypedMetric`] directly, as associated constants are not
5151
// object safe and thus can not be used with dynamic dispatching.
5252
fn metric_type(&self) -> MetricType;
53+
54+
/// Check if the metric is empty.
55+
///
56+
/// An empty metric is a metric that has no data to encode, and thus should not have any
57+
/// descriptor in the final output.
58+
///
59+
/// By default, this returns `false`, ensuring the metric and its description is always
60+
/// encoded.
61+
fn is_empty(&self) -> bool {
62+
false
63+
}
5364
}
5465

5566
impl EncodeMetric for Box<dyn EncodeMetric> {

src/encoding/text.rs

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,21 +1251,21 @@ mod tests {
12511251
}
12521252

12531253
fn parse_with_python_client(input: String) {
1254-
pyo3::prepare_freethreaded_python();
1254+
pyo3::Python::initialize();
12551255

12561256
println!("{input:?}");
1257-
Python::with_gil(|py| {
1258-
let parser = PyModule::from_code_bound(
1257+
Python::attach(|py| {
1258+
let parser = PyModule::from_code(
12591259
py,
1260-
r#"
1260+
c"
12611261
from prometheus_client.openmetrics.parser import text_string_to_metric_families
12621262
12631263
def parse(input):
12641264
families = text_string_to_metric_families(input)
12651265
list(families)
1266-
"#,
1267-
"parser.py",
1268-
"parser",
1266+
",
1267+
c"parser.py",
1268+
c"parser",
12691269
)
12701270
.map_err(|e| e.to_string())
12711271
.unwrap();
@@ -1278,4 +1278,41 @@ def parse(input):
12781278
.unwrap();
12791279
})
12801280
}
1281+
1282+
#[test]
1283+
fn encode_omit_empty() {
1284+
let mut registry = Registry::default();
1285+
let counter1: Family<Vec<(&'static str, &'static str)>, Counter> = Default::default();
1286+
let counter2: Family<Vec<(&'static str, &'static str)>, Counter> = Default::default();
1287+
let counter3: Family<Vec<(&'static str, &'static str)>, Counter> = Default::default();
1288+
1289+
registry.register("counter1", "First counter", counter1.clone());
1290+
registry.register("counter2", "Second counter", counter2.clone());
1291+
registry.register("counter3", "Third counter", counter3.clone());
1292+
1293+
counter1.get_or_create(&vec![("label", "value")]).inc();
1294+
1295+
let mut encoded = String::new();
1296+
encode(&mut encoded, &registry).unwrap();
1297+
1298+
let expected = "# HELP counter1 First counter.\n".to_owned()
1299+
+ "# TYPE counter1 counter\n"
1300+
+ "counter1_total{label=\"value\"} 1\n"
1301+
+ "# EOF\n";
1302+
assert_eq!(expected, encoded);
1303+
1304+
counter2.get_or_create(&vec![("label", "value")]).inc();
1305+
1306+
let mut encoded = String::new();
1307+
encode(&mut encoded, &registry).unwrap();
1308+
1309+
let expected = "# HELP counter1 First counter.\n".to_owned()
1310+
+ "# TYPE counter1 counter\n"
1311+
+ "counter1_total{label=\"value\"} 1\n"
1312+
+ "# HELP counter2 Second counter.\n"
1313+
+ "# TYPE counter2 counter\n"
1314+
+ "counter2_total{label=\"value\"} 1\n"
1315+
+ "# EOF\n";
1316+
assert_eq!(expected, encoded);
1317+
}
12811318
}

src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@
7777
//! ```
7878
//! See [examples] directory for more.
7979
//!
80+
//! # Features
81+
//!
82+
//! The `test-util` gates additional APIs, such as accessors, to facilitate integration and unit
83+
//! testing of metrics. Note that APIs gated by this feature flag are not subject to stability
84+
//! guarantees and may be changed or removed in patch releases.
85+
//!
8086
//! [examples]: https://github.com/prometheus/client_rust/tree/master/examples
8187
8288
pub mod collector;

src/metrics/counter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ mod tests {
265265
// Map infinite, subnormal and NaN to 0.0.
266266
.map(|f| if f.is_normal() { f } else { 0.0 })
267267
.collect();
268-
let sum = fs.iter().sum();
268+
let sum: f64 = fs.iter().sum();
269269
let counter = Counter::<f64, AtomicU64>::default();
270270
for f in fs {
271271
counter.inc_by(f);

src/metrics/family.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,56 @@ impl<S: Clone + std::hash::Hash + Eq, M, C: MetricConstructor<M>> Family<S, M, C
341341
self.metrics.write().clear()
342342
}
343343

344+
/// Returns `true` if the given label set exists within the metric family.
345+
///
346+
/// ```
347+
/// # use prometheus_client::metrics::counter::{Atomic, Counter};
348+
/// # use prometheus_client::metrics::family::Family;
349+
/// #
350+
/// let family = Family::<Vec<(String, String)>, Counter>::default();
351+
/// let get = vec![("method".to_owned(), "GET".to_owned())];
352+
/// let post = vec![("method".to_owned(), "POST".to_owned())];
353+
///
354+
/// // Create the metric with labels `method="GET"`.
355+
/// family.get_or_create(&get).inc();
356+
///
357+
/// assert!(family.contains(&get), "a `method=\"GET\"`-labeled metric exists");
358+
/// assert!(!family.contains(&post), "a `method=\"POST\"`-labeled metric does NOT exist");
359+
/// ```
360+
#[cfg(any(test, feature = "test-util"))]
361+
pub fn contains(&self, label_set: &S) -> bool {
362+
self.metrics.read().get(label_set).is_some()
363+
}
364+
365+
/// Returns the number of metrics in this family.
366+
///
367+
/// ```
368+
/// # use prometheus_client::metrics::counter::{Atomic, Counter};
369+
/// # use prometheus_client::metrics::family::Family;
370+
/// #
371+
/// let family = Family::<Vec<(String, String)>, Counter>::default();
372+
/// assert_eq!(family.len(), 0);
373+
///
374+
/// // Will create the metric with label `method="GET"` on first call and
375+
/// // return a reference.
376+
/// family.get_or_create(&vec![("method".to_owned(), "GET".to_owned())]).inc();
377+
/// assert_eq!(family.len(), 1);
378+
///
379+
/// // Clear the family of all label sets.
380+
/// family.clear();
381+
/// assert_eq!(family.len(), 0);
382+
/// ```
383+
#[cfg(any(test, feature = "test-util"))]
384+
pub fn len(&self) -> usize {
385+
self.metrics.read().len()
386+
}
387+
388+
/// Returns `true` if the family contains no metrics.
389+
#[cfg(any(test, feature = "test-util"))]
390+
pub fn is_empty(&self) -> bool {
391+
self.metrics.read().is_empty()
392+
}
393+
344394
pub(crate) fn read(&self) -> RwLockReadGuard<'_, HashMap<S, M>> {
345395
self.metrics.read()
346396
}
@@ -377,6 +427,10 @@ where
377427
fn metric_type(&self) -> MetricType {
378428
M::TYPE
379429
}
430+
431+
fn is_empty(&self) -> bool {
432+
self.metrics.read().is_empty()
433+
}
380434
}
381435

382436
#[cfg(test)]

src/metrics/histogram.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ impl Histogram {
8181
self.observe_and_bucket(v);
8282
}
8383

84+
/// Returns the current sum of all observations.
85+
#[cfg(any(test, feature = "test-util"))]
86+
pub fn sum(&self) -> f64 {
87+
self.inner.read().sum
88+
}
89+
90+
/// Returns the current number of observations.
91+
#[cfg(any(test, feature = "test-util"))]
92+
pub fn count(&self) -> u64 {
93+
self.inner.read().count
94+
}
95+
8496
/// Observes the given value, returning the index of the first bucket the
8597
/// value is added to.
8698
///
@@ -212,4 +224,53 @@ mod tests {
212224
let res = exponential_buckets_range(0.0, 32.0, 6).collect::<Vec<_>>();
213225
assert!(res.is_empty());
214226
}
227+
228+
/// Checks that [`Histogram::count()`] works properly.
229+
#[test]
230+
fn count() {
231+
let histogram = Histogram::new([1.0_f64, 2.0, 3.0, 4.0, 5.0]);
232+
assert_eq!(
233+
histogram.count(),
234+
0,
235+
"histogram has zero observations when instantiated"
236+
);
237+
238+
histogram.observe(1.0);
239+
assert_eq!(histogram.count(), 1, "histogram has one observation");
240+
241+
histogram.observe(2.5);
242+
assert_eq!(histogram.count(), 2, "histogram has two observations");
243+
244+
histogram.observe(6.0);
245+
assert_eq!(histogram.count(), 3, "histogram has three observations");
246+
}
247+
248+
/// Checks that [`Histogram::sum()`] works properly.
249+
#[test]
250+
fn sum() {
251+
const BUCKETS: [f64; 3] = [10.0, 100.0, 1000.0];
252+
let histogram = Histogram::new(BUCKETS);
253+
assert_eq!(
254+
histogram.sum(),
255+
0.0,
256+
"histogram sum is zero when instantiated"
257+
);
258+
259+
histogram.observe(3.0); // 3 + 4 + 15 + 101 = 123
260+
histogram.observe(4.0);
261+
histogram.observe(15.0);
262+
histogram.observe(101.0);
263+
assert_eq!(
264+
histogram.sum(),
265+
123.0,
266+
"histogram sum records accurate sum of observations"
267+
);
268+
269+
histogram.observe(1111.0);
270+
assert_eq!(
271+
histogram.sum(),
272+
1234.0,
273+
"histogram sum records accurate sum of observations"
274+
);
275+
}
215276
}

src/registry.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,15 +228,15 @@ impl Registry {
228228
/// let subsystem_a_counter_2: Counter = Counter::default();
229229
///
230230
/// let subsystem_a_registry = registry.sub_registry_with_prefix("subsystem_a");
231-
/// registry.register("counter_1", "", subsystem_a_counter_1.clone());
232-
/// registry.register("counter_2", "", subsystem_a_counter_2.clone());
231+
/// subsystem_a_registry.register("counter_1", "", subsystem_a_counter_1.clone());
232+
/// subsystem_a_registry.register("counter_2", "", subsystem_a_counter_2.clone());
233233
///
234234
/// let subsystem_b_counter_1: Counter = Counter::default();
235235
/// let subsystem_b_counter_2: Counter = Counter::default();
236236
///
237-
/// let subsystem_a_registry = registry.sub_registry_with_prefix("subsystem_b");
238-
/// registry.register("counter_1", "", subsystem_b_counter_1.clone());
239-
/// registry.register("counter_2", "", subsystem_b_counter_2.clone());
237+
/// let subsystem_b_registry = registry.sub_registry_with_prefix("subsystem_b");
238+
/// subsystem_b_registry.register("counter_1", "", subsystem_b_counter_1.clone());
239+
/// subsystem_b_registry.register("counter_2", "", subsystem_b_counter_2.clone());
240240
/// ```
241241
///
242242
/// See [`Registry::sub_registry_with_label`] for the same functionality,
@@ -287,7 +287,7 @@ impl Registry {
287287
}
288288

289289
pub(crate) fn encode(&self, encoder: &mut DescriptorEncoder) -> Result<(), std::fmt::Error> {
290-
for (descriptor, metric) in self.metrics.iter() {
290+
for (descriptor, metric) in self.metrics.iter().filter(|(_, m)| !m.is_empty()) {
291291
let mut descriptor_encoder =
292292
encoder.with_prefix_and_labels(self.prefix.as_ref(), &self.labels);
293293
let metric_encoder = descriptor_encoder.encode_descriptor(

0 commit comments

Comments
 (0)