1use std::collections::HashMap;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::sync::{Arc, Weak};
18
19use parking_lot::Mutex;
20use prometheus::core::{Collector, MetricVec, MetricVecBuilder};
21use prometheus::{HistogramVec, IntCounterVec};
22
23use crate::{
24 LabelGuardedHistogramVec, LabelGuardedIntCounterVec, LabelGuardedIntGaugeVec,
25 LabelGuardedMetric, LabelGuardedMetricVec, LazyLabelGuardedMetrics, MetricLevel,
26};
27
28#[derive(Clone, Debug)]
49pub struct RelabeledMetricVec<M> {
50 relabel_threshold: MetricLevel,
51 metric_level: MetricLevel,
52 metric: M,
53
54 relabel_num: usize,
60}
61
62impl<M> RelabeledMetricVec<M> {
63 pub fn with_metric_level(
64 metric_level: MetricLevel,
65 metric: M,
66 relabel_threshold: MetricLevel,
67 ) -> Self {
68 Self {
69 relabel_threshold,
70 metric_level,
71 metric,
72 relabel_num: usize::MAX,
73 }
74 }
75
76 pub fn with_metric_level_relabel_n(
77 metric_level: MetricLevel,
78 metric: M,
79 relabel_threshold: MetricLevel,
80 relabel_num: usize,
81 ) -> Self {
82 Self {
83 relabel_threshold,
84 metric_level,
85 metric,
86 relabel_num,
87 }
88 }
89
90 fn relabel_impl<V: AsRef<str> + std::fmt::Debug>(&self, vals: &[V]) -> Option<Vec<String>> {
91 if self.metric_level > self.relabel_threshold {
92 let mut relabeled_vals = vals
94 .iter()
95 .map(|v| v.as_ref().to_owned())
96 .collect::<Vec<_>>();
97 for label in relabeled_vals.iter_mut().take(self.relabel_num) {
98 *label = String::new();
99 }
100 Some(relabeled_vals)
101 } else {
102 None
103 }
104 }
105}
106
107#[easy_ext::ext(MetricVecRelabelExt)]
108impl<M> M
109where
110 M: Sized,
111{
112 pub fn relabel(
114 self,
115 metric_level: MetricLevel,
116 relabel_threshold: MetricLevel,
117 ) -> RelabeledMetricVec<M> {
118 RelabeledMetricVec::with_metric_level(metric_level, self, relabel_threshold)
119 }
120
121 pub fn relabel_n(
123 self,
124 metric_level: MetricLevel,
125 relabel_threshold: MetricLevel,
126 relabel_num: usize,
127 ) -> RelabeledMetricVec<M> {
128 RelabeledMetricVec::with_metric_level_relabel_n(
129 metric_level,
130 self,
131 relabel_threshold,
132 relabel_num,
133 )
134 }
135
136 pub fn relabel_debug_1(self, relabel_threshold: MetricLevel) -> RelabeledMetricVec<M> {
139 RelabeledMetricVec::with_metric_level_relabel_n(
140 MetricLevel::Debug,
141 self,
142 relabel_threshold,
143 1,
144 )
145 }
146}
147
148impl<T: MetricVecBuilder> RelabeledMetricVec<MetricVec<T>> {
149 pub fn with_label_values<V: AsRef<str> + std::fmt::Debug>(&self, vals: &[V]) -> T::M {
150 if let Some(relabeled_vals) = self.relabel_impl(vals) {
151 return self.metric.with_label_values(&relabeled_vals);
152 }
153 self.metric.with_label_values(vals)
154 }
155
156 pub fn reset(&self) {
157 self.metric.reset();
158 }
159}
160
161impl<T: MetricVecBuilder> RelabeledMetricVec<LabelGuardedMetricVec<T>> {
162 pub fn with_guarded_label_values<V: AsRef<str> + std::fmt::Debug>(
163 &self,
164 vals: &[V],
165 ) -> LabelGuardedMetric<T::M> {
166 if let Some(relabeled_vals) = self.relabel_impl(vals) {
167 return self.metric.with_guarded_label_values(&relabeled_vals);
168 }
169 self.metric.with_guarded_label_values(vals)
170 }
171
172 pub fn lazy_guarded_metrics(&self, labels: Vec<String>) -> LazyLabelGuardedMetrics<T> {
173 if let Some(relabeled_vals) = self.relabel_impl(labels.as_slice()) {
174 return self.metric.clone().lazy_guarded_metrics(relabeled_vals);
175 }
176 self.metric.clone().lazy_guarded_metrics(labels)
177 }
178}
179
180impl<T: Collector> Collector for RelabeledMetricVec<T> {
181 fn desc(&self) -> Vec<&prometheus::core::Desc> {
182 self.metric.desc()
183 }
184
185 fn collect(&self) -> Vec<prometheus::proto::MetricFamily> {
186 self.metric.collect()
187 }
188}
189
190pub type RelabeledCounterVec = RelabeledMetricVec<IntCounterVec>;
191pub type RelabeledHistogramVec = RelabeledMetricVec<HistogramVec>;
192
193pub type RelabeledGuardedHistogramVec = RelabeledMetricVec<LabelGuardedHistogramVec>;
194pub type RelabeledGuardedIntCounterVec = RelabeledMetricVec<LabelGuardedIntCounterVec>;
195
196pub type RelabeledGuardedIntGaugeVec = RelabeledMetricVec<LabelGuardedIntGaugeVec>;
200
201#[derive(Clone, Copy, Debug, PartialEq, Eq)]
203pub enum GaugeAggregation {
204 Sum,
205 Min,
206 Max,
207}
208
209pub trait IntGaugeVecRelabelExt {
211 fn relabel_debug_1_with_aggregation(
214 self,
215 metric_level: MetricLevel,
216 aggregation: GaugeAggregation,
217 ) -> RelabeledAggregatedIntGaugeVec;
218}
219
220impl IntGaugeVecRelabelExt for LabelGuardedIntGaugeVec {
221 fn relabel_debug_1_with_aggregation(
222 self,
223 metric_level: MetricLevel,
224 aggregation: GaugeAggregation,
225 ) -> RelabeledAggregatedIntGaugeVec {
226 RelabeledAggregatedIntGaugeVec {
227 metric: RelabeledMetricVec::with_metric_level_relabel_n(
228 MetricLevel::Debug,
229 self,
230 metric_level,
231 1,
232 ),
233 aggregation,
234 groups: Arc::new(AggregatedGaugeGroups::default()),
235 }
236 }
237}
238
239#[derive(Clone, Debug)]
241pub struct RelabeledAggregatedIntGaugeVec {
242 metric: RelabeledGuardedIntGaugeVec,
243 aggregation: GaugeAggregation,
244 groups: Arc<AggregatedGaugeGroups>,
245}
246
247impl RelabeledAggregatedIntGaugeVec {
248 pub fn with_guarded_label_values<V: AsRef<str> + std::fmt::Debug>(
249 &self,
250 vals: &[V],
251 ) -> RelabeledAggregatedIntGauge {
252 let relabeled_vals = self.metric.relabel_impl(vals);
253 let first_label_is_empty = match &relabeled_vals {
254 Some(labels) => labels.first().is_some_and(String::is_empty),
255 None => vals.first().is_some_and(|label| label.as_ref().is_empty()),
256 };
257
258 if !first_label_is_empty {
259 let gauge = match relabeled_vals {
260 Some(labels) => self.metric.metric.with_guarded_label_values(&labels),
261 None => self.metric.metric.with_guarded_label_values(vals),
262 };
263 return RelabeledAggregatedIntGauge {
264 inner: RelabeledAggregatedIntGaugeInner::Passthrough(gauge),
265 };
266 }
267
268 let labels = relabeled_vals
269 .unwrap_or_else(|| vals.iter().map(|label| label.as_ref().to_owned()).collect())
270 .into_boxed_slice();
271 let group = self
272 .groups
273 .get_or_create(labels, self.aggregation, |labels| {
274 self.metric.metric.with_guarded_label_values(labels)
275 });
276 let id = self.groups.next_id.fetch_add(1, Ordering::Relaxed);
277 RelabeledAggregatedIntGauge {
278 inner: RelabeledAggregatedIntGaugeInner::Aggregated(Arc::new(
279 AggregatedGaugeContribution { id, group },
280 )),
281 }
282 }
283}
284
285impl Collector for RelabeledAggregatedIntGaugeVec {
286 fn desc(&self) -> Vec<&prometheus::core::Desc> {
287 self.metric.desc()
288 }
289
290 fn collect(&self) -> Vec<prometheus::proto::MetricFamily> {
291 self.metric.collect()
292 }
293}
294
295#[derive(Debug, Default)]
296struct AggregatedGaugeGroups {
297 inner: Mutex<HashMap<Box<[String]>, Weak<AggregatedGaugeGroup>>>,
298 next_id: AtomicU64,
299}
300
301impl AggregatedGaugeGroups {
302 fn get_or_create(
303 self: &Arc<Self>,
304 labels: Box<[String]>,
305 aggregation: GaugeAggregation,
306 create_gauge: impl FnOnce(&[String]) -> LabelGuardedMetric<prometheus::IntGauge>,
307 ) -> Arc<AggregatedGaugeGroup> {
308 let mut groups = self.inner.lock();
309 if let Some(group) = groups.get(&labels).and_then(Weak::upgrade) {
310 return group;
311 }
312
313 let group = Arc::new(AggregatedGaugeGroup {
314 gauge: create_gauge(&labels),
315 aggregation,
316 state: Mutex::new(AggregatedGaugeState::default()),
317 labels: labels.clone(),
318 groups: Arc::downgrade(self),
319 });
320 groups.insert(labels, Arc::downgrade(&group));
321 group
322 }
323}
324
325#[derive(Debug, Default)]
326struct AggregatedGaugeState {
327 values: HashMap<u64, i64>,
328 aggregate: Option<i64>,
329}
330
331#[derive(Debug)]
332struct AggregatedGaugeGroup {
333 gauge: LabelGuardedMetric<prometheus::IntGauge>,
334 aggregation: GaugeAggregation,
335 state: Mutex<AggregatedGaugeState>,
336 labels: Box<[String]>,
337 groups: Weak<AggregatedGaugeGroups>,
338}
339
340impl AggregatedGaugeGroup {
341 fn set(&self, id: u64, value: i64) {
342 let mut state = self.state.lock();
343 self.set_locked(&mut state, id, value);
344 }
345
346 fn set_locked(&self, state: &mut AggregatedGaugeState, id: u64, value: i64) {
347 let previous = state.values.insert(id, value);
348 let aggregate = match self.aggregation {
349 GaugeAggregation::Sum => state
350 .aggregate
351 .unwrap_or(0)
352 .wrapping_sub(previous.unwrap_or(0))
353 .wrapping_add(value),
354 GaugeAggregation::Min => {
355 if previous == state.aggregate && previous.is_some_and(|old| value > old) {
356 *state
357 .values
358 .values()
359 .min()
360 .expect("the updated value exists")
361 } else {
362 state.aggregate.map_or(value, |current| current.min(value))
363 }
364 }
365 GaugeAggregation::Max => {
366 if previous == state.aggregate && previous.is_some_and(|old| value < old) {
367 *state
368 .values
369 .values()
370 .max()
371 .expect("the updated value exists")
372 } else {
373 state.aggregate.map_or(value, |current| current.max(value))
374 }
375 }
376 };
377 state.aggregate = Some(aggregate);
378 self.gauge.set(aggregate);
379 }
380
381 fn add(&self, id: u64, delta: i64) {
382 let mut state = self.state.lock();
383 let current = state.values.get(&id).copied().unwrap_or(0);
384 self.set_locked(&mut state, id, current.wrapping_add(delta));
385 }
386
387 fn get(&self) -> i64 {
388 self.state.lock().aggregate.unwrap_or(0)
389 }
390
391 fn remove(&self, id: u64) {
392 let mut state = self.state.lock();
393 let Some(previous) = state.values.remove(&id) else {
394 return;
395 };
396 let aggregate = match self.aggregation {
397 GaugeAggregation::Sum => state.aggregate.unwrap_or(0).wrapping_sub(previous),
398 GaugeAggregation::Min => state.values.values().copied().min().unwrap_or(0),
399 GaugeAggregation::Max => state.values.values().copied().max().unwrap_or(0),
400 };
401 state.aggregate = (!state.values.is_empty()).then_some(aggregate);
402 self.gauge.set(aggregate);
403 }
404}
405
406impl Drop for AggregatedGaugeGroup {
407 fn drop(&mut self) {
408 let Some(groups) = self.groups.upgrade() else {
409 return;
410 };
411 let mut entries = groups.inner.lock();
412 if entries
413 .get(&self.labels)
414 .is_some_and(|entry| std::ptr::eq(entry.as_ptr(), self))
415 {
416 entries.remove(&self.labels);
417 }
418 }
419}
420
421#[derive(Clone, Debug)]
422pub struct RelabeledAggregatedIntGauge {
423 inner: RelabeledAggregatedIntGaugeInner,
424}
425
426#[derive(Clone, Debug)]
427enum RelabeledAggregatedIntGaugeInner {
428 Passthrough(LabelGuardedMetric<prometheus::IntGauge>),
429 Aggregated(Arc<AggregatedGaugeContribution>),
430}
431
432#[derive(Debug)]
433struct AggregatedGaugeContribution {
434 id: u64,
435 group: Arc<AggregatedGaugeGroup>,
436}
437
438impl Drop for AggregatedGaugeContribution {
439 fn drop(&mut self) {
440 self.group.remove(self.id);
441 }
442}
443
444impl RelabeledAggregatedIntGauge {
445 pub fn test_int_gauge<const N: usize>(_aggregation: GaugeAggregation) -> Self {
447 Self {
448 inner: RelabeledAggregatedIntGaugeInner::Passthrough(
449 crate::LabelGuardedIntGauge::test_int_gauge::<N>(),
450 ),
451 }
452 }
453
454 pub fn set(&self, value: i64) {
455 match &self.inner {
456 RelabeledAggregatedIntGaugeInner::Passthrough(gauge) => gauge.set(value),
457 RelabeledAggregatedIntGaugeInner::Aggregated(contribution) => {
458 contribution.group.set(contribution.id, value);
459 }
460 }
461 }
462
463 pub fn set_optional(&self, value: Option<i64>) {
466 match &self.inner {
467 RelabeledAggregatedIntGaugeInner::Passthrough(gauge) => {
468 gauge.set(value.unwrap_or_default());
469 }
470 RelabeledAggregatedIntGaugeInner::Aggregated(contribution) => match value {
471 Some(value) => contribution.group.set(contribution.id, value),
472 None => contribution.group.remove(contribution.id),
473 },
474 }
475 }
476
477 pub fn add(&self, value: i64) {
478 match &self.inner {
479 RelabeledAggregatedIntGaugeInner::Passthrough(gauge) => gauge.add(value),
480 RelabeledAggregatedIntGaugeInner::Aggregated(contribution) => {
481 contribution.group.add(contribution.id, value);
482 }
483 }
484 }
485
486 pub fn sub(&self, value: i64) {
487 self.add(value.wrapping_neg());
488 }
489
490 pub fn inc(&self) {
491 self.add(1);
492 }
493
494 pub fn dec(&self) {
495 self.sub(1);
496 }
497
498 pub fn get(&self) -> i64 {
499 match &self.inner {
500 RelabeledAggregatedIntGaugeInner::Passthrough(gauge) => gauge.get(),
501 RelabeledAggregatedIntGaugeInner::Aggregated(contribution) => contribution.group.get(),
502 }
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use prometheus::Registry;
509
510 use super::*;
511
512 fn test_vec(
513 registry: &Registry,
514 level: MetricLevel,
515 aggregation: GaugeAggregation,
516 ) -> RelabeledAggregatedIntGaugeVec {
517 crate::register_guarded_int_gauge_vec_with_registry!(
518 "test_relabel_aggregated_gauge",
519 "test",
520 &["actor_id", "key"],
521 registry,
522 )
523 .unwrap()
524 .relabel_debug_1_with_aggregation(level, aggregation)
525 }
526
527 fn values(registry: &Registry) -> Vec<i64> {
528 let Some(family) = registry
529 .gather()
530 .into_iter()
531 .find(|family| family.name() == "test_relabel_aggregated_gauge")
532 else {
533 return vec![];
534 };
535 family
536 .get_metric()
537 .iter()
538 .map(|metric| metric.get_gauge().value() as i64)
539 .collect()
540 }
541
542 #[test]
543 fn test_sum_aggregation_update_clone_and_drop() {
544 for level in [
545 MetricLevel::Disabled,
546 MetricLevel::Critical,
547 MetricLevel::Info,
548 ] {
549 let registry = Registry::new();
550 let vec = test_vec(®istry, level, GaugeAggregation::Sum);
551 let actor_1 = vec.with_guarded_label_values(&["1", "shared"]);
552 let actor_2 = vec.with_guarded_label_values(&["2", "shared"]);
553
554 actor_1.set(5);
555 actor_2.set(7);
556 assert_eq!(values(®istry), [12]);
557
558 actor_1.set(9);
559 assert_eq!(values(®istry), [16]);
560
561 let actor_1_clone = actor_1.clone();
562 drop(actor_1);
563 assert_eq!(values(®istry), [16]);
564 drop(actor_1_clone);
565 assert_eq!(values(®istry), [7]);
566
567 drop(actor_2);
568 assert_eq!(values(®istry), [0]);
569 assert!(values(®istry).is_empty());
570 }
571 }
572
573 #[test]
574 fn test_min_aggregation_ignores_uninitialized_handles() {
575 for level in [
576 MetricLevel::Disabled,
577 MetricLevel::Critical,
578 MetricLevel::Info,
579 ] {
580 let registry = Registry::new();
581 let vec = test_vec(®istry, level, GaugeAggregation::Min);
582 let actor_1 = vec.with_guarded_label_values(&["1", "shared"]);
583 let actor_2 = vec.with_guarded_label_values(&["2", "shared"]);
584
585 actor_1.set(100);
586 assert_eq!(values(®istry), [100]);
587 actor_2.set(200);
588 assert_eq!(values(®istry), [100]);
589 actor_1.set(300);
590 assert_eq!(values(®istry), [200]);
591 drop(actor_2);
592 assert_eq!(values(®istry), [300]);
593 }
594 }
595
596 #[test]
597 fn test_min_aggregation_ignores_absent_contributions() {
598 for level in [MetricLevel::Critical, MetricLevel::Info] {
599 let registry = Registry::new();
600 let vec = test_vec(®istry, level, GaugeAggregation::Min);
601 let empty_actor = vec.with_guarded_label_values(&["1", "shared"]);
602 let pending_actor = vec.with_guarded_label_values(&["2", "shared"]);
603
604 empty_actor.set_optional(None);
605 pending_actor.set_optional(Some(100));
606 assert_eq!(values(®istry), [100]);
607
608 empty_actor.set_optional(Some(200));
609 assert_eq!(values(®istry), [100]);
610
611 pending_actor.set_optional(None);
612 assert_eq!(values(®istry), [200]);
613
614 empty_actor.set_optional(None);
615 assert_eq!(values(®istry), [0]);
616 }
617 }
618
619 #[test]
620 fn test_max_aggregation_update_clone_and_drop() {
621 for level in [
622 MetricLevel::Disabled,
623 MetricLevel::Critical,
624 MetricLevel::Info,
625 ] {
626 let registry = Registry::new();
627 let vec = test_vec(®istry, level, GaugeAggregation::Max);
628 let actor_1 = vec.with_guarded_label_values(&["1", "shared"]);
629 let actor_2 = vec.with_guarded_label_values(&["2", "shared"]);
630
631 actor_1.set(100);
632 assert_eq!(values(®istry), [100]);
633 actor_2.set(200);
634 assert_eq!(values(®istry), [200]);
635 actor_1.set(300);
636 assert_eq!(values(®istry), [300]);
637 actor_2.set(300);
638 actor_1.set(100);
639 assert_eq!(values(®istry), [300]);
640 actor_2.set(50);
641 assert_eq!(values(®istry), [100]);
642
643 let actor_1_clone = actor_1.clone();
644 drop(actor_1);
645 assert_eq!(values(®istry), [100]);
646 drop(actor_1_clone);
647 assert_eq!(values(®istry), [50]);
648 drop(actor_2);
649 assert_eq!(values(®istry), [0]);
650 assert!(values(®istry).is_empty());
651 }
652 }
653
654 #[test]
655 fn test_max_aggregation_ignores_absent_contributions() {
656 for level in [MetricLevel::Critical, MetricLevel::Info] {
657 let registry = Registry::new();
658 let vec = test_vec(®istry, level, GaugeAggregation::Max);
659 let actor_1 = vec.with_guarded_label_values(&["1", "shared"]);
660 let actor_2 = vec.with_guarded_label_values(&["2", "shared"]);
661
662 actor_1.set(-100);
664 assert_eq!(values(®istry), [-100]);
665 actor_2.set_optional(None);
666 assert_eq!(values(®istry), [-100]);
667 actor_2.set_optional(Some(-50));
668 assert_eq!(values(®istry), [-50]);
669 actor_2.set_optional(None);
670 assert_eq!(values(®istry), [-100]);
671 actor_1.set_optional(None);
672 assert_eq!(values(®istry), [0]);
673 actor_2.set(-200);
674 assert_eq!(values(®istry), [-200]);
675 }
676 }
677
678 #[test]
679 fn test_debug_passthrough_and_distinct_groups() {
680 let registry = Registry::new();
681 let vec = test_vec(®istry, MetricLevel::Debug, GaugeAggregation::Sum);
682 let actor_1 = vec.with_guarded_label_values(&["1", "shared"]);
683 let actor_2 = vec.with_guarded_label_values(&["2", "shared"]);
684 actor_1.set(5);
685 actor_2.set(7);
686 let mut collected = values(®istry);
687 collected.sort_unstable();
688 assert_eq!(collected, [5, 7]);
689 let mut actor_ids = registry
690 .gather()
691 .into_iter()
692 .find(|family| family.name() == "test_relabel_aggregated_gauge")
693 .unwrap()
694 .get_metric()
695 .iter()
696 .map(|metric| crate::get_label::<String>(metric, "actor_id").unwrap())
697 .collect::<Vec<_>>();
698 actor_ids.sort_unstable();
699 assert_eq!(actor_ids, ["1", "2"]);
700
701 let registry = Registry::new();
702 let vec = test_vec(®istry, MetricLevel::Info, GaugeAggregation::Sum);
703 let first = vec.with_guarded_label_values(&["1", "first"]);
704 let second = vec.with_guarded_label_values(&["2", "second"]);
705 first.set(5);
706 second.set(7);
707 let mut collected = values(®istry);
708 collected.sort_unstable();
709 assert_eq!(collected, [5, 7]);
710 }
711
712 #[test]
713 fn test_aggregation_follows_the_effective_first_label() {
714 let registry = Registry::new();
715 let vec = test_vec(®istry, MetricLevel::Debug, GaugeAggregation::Sum);
716 let first = vec.with_guarded_label_values(&["", "shared"]);
717 let second = vec.with_guarded_label_values(&["", "shared"]);
718
719 first.set(5);
720 second.set(7);
721
722 assert_eq!(values(®istry), [12]);
723 }
724
725 #[test]
726 fn test_concurrent_adds_and_sets_keep_exported_value_consistent() {
727 let registry = Registry::new();
728 let vec = test_vec(®istry, MetricLevel::Info, GaugeAggregation::Sum);
729 let gauge = vec.with_guarded_label_values(&["1", "shared"]);
730 let threads = (0..4)
731 .map(|thread_index| {
732 let gauge = gauge.clone();
733 std::thread::spawn(move || {
734 for value in 0..10_000 {
735 if thread_index % 2 == 0 {
736 gauge.add(1);
737 } else {
738 gauge.set(value);
739 }
740 }
741 })
742 })
743 .collect::<Vec<_>>();
744 for thread in threads {
745 thread.join().unwrap();
746 }
747 assert_eq!(values(®istry), [gauge.get()]);
748 }
749}