Skip to content

Commit a6babae

Browse files
committed
feat: add retain() to Family to allow metrics filtering
Signed-off-by: John Howard <john.howard@solo.io>
1 parent 9a74e99 commit a6babae

File tree

3 files changed

+123
-0
lines changed

3 files changed

+123
-0
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ http-body-util = "0.1.1"
4444
[build-dependencies]
4545
prost-build = { version = "0.12.0", optional = true }
4646

47+
[[example]]
48+
name = "prune"
49+
4750
[[bench]]
4851
name = "baseline"
4952
harness = false

examples/prune.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use prometheus_client::encoding::{text::encode, EncodeMetric, MetricEncoder};
2+
use prometheus_client::metrics::counter::Atomic;
3+
use prometheus_client::metrics::family::Family;
4+
use prometheus_client::metrics::{MetricType, TypedMetric};
5+
use prometheus_client::registry::Registry;
6+
use prometheus_client_derive_encode::EncodeLabelSet;
7+
use std::fmt::Error;
8+
use std::sync::atomic::AtomicU64;
9+
use std::sync::{Arc, Mutex};
10+
use std::thread;
11+
use std::time::Duration;
12+
use tokio::time::Instant;
13+
14+
// The 'prune' example shows an advanced use case of a custom metric type that records it's last record date.
15+
// Then, label sets older than a certain time period are removed from the label set.
16+
fn main() {
17+
let mut registry = Registry::default();
18+
19+
let metric: Family<Labels, MyCounter> = Family::default();
20+
registry.register("my_custom_metric", "test", metric.clone());
21+
// First we record two label sets, apple and banana.
22+
metric
23+
.get_or_create(&Labels {
24+
name: "apple".to_string(),
25+
})
26+
.inc();
27+
metric
28+
.get_or_create(&Labels {
29+
name: "banana".to_string(),
30+
})
31+
.inc();
32+
33+
let mut encoded = String::new();
34+
encode(&mut encoded, &registry).unwrap();
35+
36+
println!("Scrape output:\n{}", encoded);
37+
thread::sleep(Duration::from_secs(1));
38+
39+
// We update only 'banana'
40+
metric
41+
.get_or_create(&Labels {
42+
name: "banana".to_string(),
43+
})
44+
.inc();
45+
let now = Instant::now();
46+
// Retain only metrics set within the last second.
47+
metric.retain(|_labels, counter| {
48+
let last = counter.last_access.lock().unwrap().unwrap();
49+
now.saturating_duration_since(last) < Duration::from_secs(1)
50+
});
51+
52+
// 'apple' should be removed now
53+
let mut encoded = String::new();
54+
encode(&mut encoded, &registry).unwrap();
55+
56+
println!("Scrape output:\n{}", encoded);
57+
}
58+
59+
#[derive(Default, Debug)]
60+
struct MyCounter {
61+
value: Arc<AtomicU64>,
62+
last_access: Arc<Mutex<Option<Instant>>>,
63+
}
64+
65+
impl TypedMetric for MyCounter {
66+
const TYPE: MetricType = MetricType::Counter;
67+
}
68+
69+
impl MyCounter {
70+
pub fn get(&self) -> u64 {
71+
self.value.get()
72+
}
73+
pub fn inc(&self) -> u64 {
74+
let mut last = self.last_access.lock().unwrap();
75+
*last = Some(Instant::now());
76+
self.value.inc()
77+
}
78+
}
79+
80+
impl EncodeMetric for MyCounter {
81+
fn encode(&self, mut encoder: MetricEncoder) -> Result<(), Error> {
82+
encoder.encode_counter::<prometheus_client::encoding::NoLabelSet, _, u64>(&self.get(), None)
83+
}
84+
85+
fn metric_type(&self) -> MetricType {
86+
todo!()
87+
}
88+
}
89+
90+
#[derive(Clone, Hash, Default, Debug, PartialEq, Eq, EncodeLabelSet)]
91+
struct Labels {
92+
name: String,
93+
}

src/metrics/family.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,33 @@ impl<S: Clone + std::hash::Hash + Eq, M, C: MetricConstructor<M>> Family<S, M, C
303303
self.metrics.write().clear()
304304
}
305305

306+
/// Retains only the label sets specified by the predicate.
307+
///
308+
/// ```
309+
/// # use prometheus_client::metrics::counter::{Atomic, Counter};
310+
/// # use prometheus_client::metrics::family::Family;
311+
/// #
312+
/// let family = Family::<Vec<(String, String)>, Counter>::default();
313+
///
314+
/// // Will create the metric with label `method="GET"` on first call and
315+
/// // return a reference.
316+
/// family.get_or_create(&vec![("method".to_owned(), "GET".to_owned())]).inc_by(10);
317+
/// family.get_or_create(&vec![("method".to_owned(), "DELETE".to_owned())]).inc();
318+
/// family.get_or_create(&vec![("method".to_owned(), "POST".to_owned())]).inc();
319+
///
320+
/// // Retain only label sets where the counter is less than 10, or method="POST"
321+
/// // This will leave the method="POST" and method="DELETE" label sets.
322+
/// family.retain(|labels, counter| {
323+
/// counter.get() < 5 || labels.contains(&("method".to_owned(), "POST".to_owned()))
324+
/// });
325+
/// ```
326+
pub fn retain<F>(&self, f: F)
327+
where
328+
F: FnMut(&S, &mut M) -> bool,
329+
{
330+
self.metrics.write().retain(f)
331+
}
332+
306333
pub(crate) fn read(&self) -> RwLockReadGuard<HashMap<S, M>> {
307334
self.metrics.read()
308335
}

0 commit comments

Comments
 (0)