1use std::any::Any;
27use std::cell::RefCell;
28use std::collections::BTreeMap;
29use std::rc::Rc;
30use std::sync::{Arc, Mutex};
31
32use differential_dataflow::{Hashable, VecCollection};
33use mz_compute_types::sinks::{ComputeSinkDesc, MetricSinkConnection};
34use mz_ore::cast::{CastFrom, CastLossy};
35use mz_repr::{ColumnName, Datum, DatumVec, Diff, GlobalId, RelationDesc, Row, Timestamp};
36use mz_storage_types::controller::CollectionMetadata;
37use mz_timely_util::probe::{Handle, ProbeNotify};
38use prometheus::core::{Collector, Desc};
39use prometheus::proto::{
40 Counter as ProtoCounter, Gauge as ProtoGauge, LabelPair, Metric as ProtoMetric, MetricFamily,
41 MetricType,
42};
43use prometheus::{Gauge, Opts};
44use timely::dataflow::channels::pact::Exchange;
45use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
46use timely::progress::Antichain;
47
48use crate::render::StartSignal;
49use crate::render::errors::DataflowErrorSer;
50use crate::render::sinks::SinkRender;
51
52impl<'scope> SinkRender<'scope> for MetricSinkConnection {
53 fn render_sink(
54 &self,
55 compute_state: &mut crate::compute_state::ComputeState,
56 sink: &ComputeSinkDesc<CollectionMetadata>,
57 sink_id: GlobalId,
58 _as_of: Antichain<Timestamp>,
59 _start_signal: StartSignal,
60 sinked_collection: VecCollection<'scope, Timestamp, Row, Diff>,
61 err_collection: VecCollection<'scope, Timestamp, DataflowErrorSer, Diff>,
62 output_probe: &Handle<Timestamp>,
63 ) -> Option<Rc<dyn Any>> {
64 let cols = ColumnIndices::resolve(&sink.from_desc);
65
66 let scope = sinked_collection.scope();
67 let worker_id = scope.index();
68 let active_worker_id = usize::cast_from(sink_id.hashed()) % scope.peers();
76
77 let ok_stream = sinked_collection
78 .inner
79 .probe_notify_with(vec![output_probe.clone()]);
80 let err_stream = err_collection.inner;
81
82 let state = Arc::new(Mutex::new(SinkState::default()));
83
84 let drop_handle = (worker_id == active_worker_id).then(|| {
96 let collector = SinkCollector::new(sink_id, Arc::clone(&state));
97 compute_state
98 .metrics_registry
99 .register_collector_with_dropper(collector)
100 });
101
102 let mut op = OperatorBuilder::new(format!("MetricSink({sink_id})"), scope);
103 let mut ok_input = op.new_input(
104 ok_stream,
105 Exchange::new(move |_: &(Row, Timestamp, Diff)| u64::cast_from(active_worker_id)),
106 );
107 let mut err_input = op.new_input(
108 err_stream,
109 Exchange::new(move |_: &(DataflowErrorSer, Timestamp, Diff)| {
110 u64::cast_from(active_worker_id)
111 }),
112 );
113
114 let sink_frontier = Rc::new(RefCell::new(Antichain::from_elem(Timestamp::MIN)));
118 let shared_frontier = Rc::clone(&sink_frontier);
119
120 op.build(move |_capabilities| {
121 let mut datum_vec = DatumVec::new();
124 move |frontiers| {
125 let mut frontier = Antichain::new();
128 for f in frontiers {
129 frontier.extend(f.frontier().iter().copied());
130 }
131 shared_frontier.borrow_mut().clone_from(&frontier);
138
139 if worker_id != active_worker_id {
140 ok_input.for_each(|_, _| {});
143 err_input.for_each(|_, _| {});
144 return;
145 }
146
147 let mut st = state.lock().expect("sink state mutex poisoned");
148
149 ok_input.for_each(|_, data| {
154 for (row, time, diff) in data.drain(..) {
155 let datums = datum_vec.borrow_with(&row);
156 let (name, metric_kind, name_valid, labels, value, help) =
157 extract_row(&cols, &datums);
158 st.stage_ok(
159 name,
160 metric_kind,
161 name_valid,
162 &labels,
163 value,
164 help,
165 time,
166 diff.into_inner(),
167 );
168 }
169 });
170 err_input.for_each(|_, data| {
171 for (_err, time, diff) in data.drain(..) {
172 st.stage_err(time, diff.into_inner());
173 }
174 });
175
176 st.integrate(&frontier);
177 st.frontier_ms = frontier
178 .as_option()
179 .map(|t| u64::from(*t))
180 .unwrap_or(u64::MAX);
181 st.publish_if_healthy();
182 }
183 });
184
185 let collection = compute_state.expect_collection_mut(sink_id);
189 collection.sink_write_frontier = Some(sink_frontier);
190
191 Some(Rc::new(drop_handle))
192 }
193}
194
195struct ColumnIndices {
203 metric_name: usize,
204 labels: usize,
205 value: usize,
206 help: usize,
207 metric_kind: usize,
208 name_valid: usize,
209}
210
211impl ColumnIndices {
212 fn resolve(desc: &RelationDesc) -> Self {
213 let idx = |name: &str| {
214 desc.get_by_name(&ColumnName::from(name))
215 .expect("column existence validated by the SQL planner")
216 .0
217 };
218 ColumnIndices {
219 metric_name: idx("metric_name"),
220 labels: idx("labels"),
221 value: idx("value"),
222 help: idx("help"),
223 metric_kind: idx("metric_kind"),
224 name_valid: idx("name_valid"),
225 }
226 }
227}
228
229fn extract_row<'a>(
237 cols: &ColumnIndices,
238 datums: &[Datum<'a>],
239) -> (
240 &'a str,
241 Option<MetricKind>,
242 bool,
243 Vec<(&'a str, &'a str)>,
244 Option<f64>,
245 &'a str,
246) {
247 let metric_name = match datums[cols.metric_name] {
248 Datum::Null => "",
249 d => d.unwrap_str(),
250 };
251 let metric_kind = MetricKind::from_datum(datums[cols.metric_kind]);
252 let name_valid = matches!(datums[cols.name_valid], Datum::True);
253 let mut labels: Vec<(&str, &str)> = datums[cols.labels]
254 .unwrap_map()
255 .iter()
256 .map(|(k, v)| (k, v.unwrap_str()))
257 .collect();
258 labels.sort();
259 let value = match datums[cols.value] {
260 Datum::Null => None,
261 d => Some(d.unwrap_float64()),
262 };
263 let help = datums[cols.help].unwrap_str();
264 (metric_name, metric_kind, name_valid, labels, value, help)
265}
266
267type RowKey = (
281 String,
282 Vec<(String, String)>,
283 Option<u64>,
284 Option<MetricKind>,
285 bool,
286 String,
287);
288
289type PublishedKey = (String, Vec<(String, String)>);
291type PublishedValue = (f64, MetricKind, String);
293
294#[derive(Default)]
305struct SinkState {
306 pending_ok: BTreeMap<Timestamp, BTreeMap<RowKey, i64>>,
308 pending_err: BTreeMap<Timestamp, i64>,
310 working: BTreeMap<RowKey, i64>,
312 published: BTreeMap<PublishedKey, PublishedValue>,
313 errors: i64,
316 frontier_ms: u64,
317 skipped: u64,
318 conflicts: u64,
319 collisions: u64,
320 null_values: u64,
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
327enum MetricKind {
328 Gauge,
329 Counter,
330}
331
332impl MetricKind {
333 fn from_datum(d: Datum) -> Option<Self> {
336 match d {
337 Datum::Int32(0) => Some(MetricKind::Gauge),
338 Datum::Int32(1) => Some(MetricKind::Counter),
339 _ => None,
340 }
341 }
342
343 fn proto_type(self) -> MetricType {
344 match self {
345 MetricKind::Gauge => MetricType::GAUGE,
346 MetricKind::Counter => MetricType::COUNTER,
347 }
348 }
349}
350
351fn is_valid_label_name(name: &str) -> bool {
357 let mut chars = name.chars();
358 match chars.next() {
359 Some(c) if c.is_ascii_alphabetic() || c == '_' => {
360 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
361 }
362 _ => false,
363 }
364}
365
366impl SinkState {
367 fn stage_ok(
374 &mut self,
375 metric_name: &str,
376 metric_kind: Option<MetricKind>,
377 name_valid: bool,
378 labels: &[(&str, &str)],
379 value: Option<f64>,
380 help: &str,
381 time: Timestamp,
382 diff: i64,
383 ) {
384 let key = (
385 metric_name.to_string(),
386 labels
387 .iter()
388 .map(|&(k, v)| (k.to_string(), v.to_string()))
389 .collect(),
390 value.map(f64::to_bits),
391 metric_kind,
392 name_valid,
393 help.to_string(),
394 );
395 *self
396 .pending_ok
397 .entry(time)
398 .or_default()
399 .entry(key)
400 .or_default() += diff;
401 }
402
403 fn stage_err(&mut self, time: Timestamp, diff: i64) {
405 *self.pending_err.entry(time).or_default() += diff;
406 }
407
408 fn integrate(&mut self, frontier: &Antichain<Timestamp>) {
415 let closed_ok: Vec<Timestamp> = self
416 .pending_ok
417 .keys()
418 .filter(|t| !frontier.less_equal(t))
419 .copied()
420 .collect();
421 for time in closed_ok {
422 let rows = self.pending_ok.remove(&time).expect("key from keys()");
423 for (key, diff) in rows {
424 *self.working.entry(key).or_default() += diff;
425 }
426 }
427
428 let closed_err: Vec<Timestamp> = self
429 .pending_err
430 .keys()
431 .filter(|t| !frontier.less_equal(t))
432 .copied()
433 .collect();
434 for time in closed_err {
435 self.errors += self.pending_err.remove(&time).expect("key from keys()");
436 }
437
438 self.working.retain(|_, acc| *acc != 0);
439 self.skipped = count_skipped(&self.working);
440 }
441
442 fn publish_if_healthy(&mut self) {
452 if self.errors == 0 {
453 let (published, collisions, null_values) = rebuild_published(&self.working);
454 self.published = published;
455 self.collisions = collisions;
456 self.null_values = null_values;
457 self.conflicts = count_conflicts(&self.published);
458 }
459 }
460}
461
462fn count_skipped(working: &BTreeMap<RowKey, i64>) -> u64 {
465 let mut skipped = 0u64;
466 for ((_name, labels, _bits, metric_kind, name_valid, _help), acc) in working {
467 if *acc <= 0 {
468 continue;
469 }
470 let unsupported = metric_kind.is_none();
471 let invalid = !name_valid || !labels.iter().all(|(k, _)| is_valid_label_name(k));
472 if unsupported || invalid {
473 skipped += 1;
474 }
475 }
476 skipped
477}
478
479fn rebuild_published(
492 working: &BTreeMap<RowKey, i64>,
493) -> (BTreeMap<PublishedKey, PublishedValue>, u64, u64) {
494 let mut grouped: BTreeMap<PublishedKey, Vec<(Option<f64>, MetricKind, String)>> =
496 BTreeMap::new();
497 for ((name, labels, bits, metric_kind, name_valid, help), acc) in working {
498 if *acc <= 0 {
499 continue;
500 }
501 let Some(kind) = metric_kind else {
502 continue;
503 };
504 if !name_valid || !labels.iter().all(|(k, _)| is_valid_label_name(k)) {
505 continue;
506 }
507 grouped
508 .entry((name.clone(), labels.clone()))
509 .or_default()
510 .push((bits.map(f64::from_bits), *kind, help.clone()));
511 }
512
513 let mut published = BTreeMap::new();
514 let mut collisions = 0u64;
515 let mut null_values = 0u64;
516 for (key, candidates) in grouped {
517 let mut non_null: Vec<PublishedValue> = candidates
518 .into_iter()
519 .filter_map(|(value, kind, help)| value.map(|v| (v, kind, help)))
520 .collect();
521 if non_null.is_empty() {
522 null_values += 1;
523 continue;
524 }
525 let mut distinct: Vec<u64> = non_null.iter().map(|(v, _, _)| v.to_bits()).collect();
526 distinct.sort_unstable();
527 distinct.dedup();
528 if distinct.len() > 1 {
529 collisions += 1;
530 }
531 non_null.sort_by(|a, b| {
532 a.0.total_cmp(&b.0)
533 .then(a.1.cmp(&b.1))
534 .then_with(|| a.2.cmp(&b.2))
535 });
536 let winner = non_null
537 .into_iter()
538 .next()
539 .expect("checked non-empty above");
540 published.insert(key, winner);
541 }
542 (published, collisions, null_values)
543}
544
545fn count_conflicts(published: &BTreeMap<PublishedKey, PublishedValue>) -> u64 {
548 let mut conflicts = 0u64;
549 let mut winner: Option<(&str, MetricKind, &str)> = None;
550 for ((name, _labels), (_value, kind, help)) in published {
551 winner = match winner {
552 Some((n, k, h)) if n == name.as_str() => {
553 if k != *kind || h != help.as_str() {
554 conflicts += 1;
555 }
556 Some((n, k, h))
557 }
558 _ => Some((name.as_str(), *kind, help.as_str())),
559 };
560 }
561 conflicts
562}
563
564fn build_families(published: &BTreeMap<PublishedKey, PublishedValue>) -> Vec<MetricFamily> {
573 let mut families = Vec::new();
574 let mut group_name: Option<&str> = None;
575 let mut family: Option<MetricFamily> = None;
576 let mut family_kind = MetricKind::Gauge;
577
578 for ((name, labels), (value, kind, help)) in published {
579 if group_name != Some(name.as_str()) {
580 if let Some(f) = family.take() {
581 families.push(f);
582 }
583 let mut mf = MetricFamily::new();
584 mf.name = Some(name.clone());
585 mf.help = Some(help.clone());
586 mf.type_ = Some(kind.proto_type().into());
587 family = Some(mf);
588 family_kind = *kind;
589 group_name = Some(name.as_str());
590 }
591
592 let mut metric = ProtoMetric::new();
593 metric.label = labels
594 .iter()
595 .map(|(k, v)| {
596 let mut lp = LabelPair::new();
597 lp.name = Some(k.clone());
598 lp.value = Some(v.clone());
599 lp
600 })
601 .collect();
602 match family_kind {
603 MetricKind::Gauge => {
604 let mut g = ProtoGauge::new();
605 g.value = Some(*value);
606 metric.gauge = Some(g).into();
607 }
608 MetricKind::Counter => {
609 let mut c = ProtoCounter::new();
610 c.value = Some(*value);
611 metric.counter = Some(c).into();
612 }
613 }
614 family
615 .as_mut()
616 .expect("initialized above for the first entry of every group")
617 .metric
618 .push(metric);
619 }
620 if let Some(f) = family.take() {
621 families.push(f);
622 }
623 families
624}
625
626#[derive(Clone)]
635struct SinkCollector {
636 state: Arc<Mutex<SinkState>>,
637 frontier_gauge: Gauge,
638 errors_gauge: Gauge,
639 skipped_gauge: Gauge,
640 conflicts_gauge: Gauge,
641 collisions_gauge: Gauge,
642 null_values_gauge: Gauge,
643}
644
645impl SinkCollector {
646 fn new(sink_id: GlobalId, state: Arc<Mutex<SinkState>>) -> Self {
647 let gauge = |name: &str, help: &str| {
648 Gauge::with_opts(Opts::new(name, help).const_label("sink", sink_id.to_string()))
649 .expect("static metric sink companion gauge options are valid")
650 };
651 SinkCollector {
652 state,
653 frontier_gauge: gauge(
654 "mz_compute_metric_sink_frontier_ms",
655 "The metric sink's input frontier, in milliseconds since the epoch.",
656 ),
657 errors_gauge: gauge(
658 "mz_compute_metric_sink_errors",
659 "The number of live errors on the metric sink's input.",
660 ),
661 skipped_gauge: gauge(
662 "mz_compute_metric_sink_skipped",
663 "The number of input rows skipped for an unsupported metric type or an invalid name.",
664 ),
665 conflicts_gauge: gauge(
666 "mz_compute_metric_sink_conflicts",
667 "The number of published series whose type or help disagree with their family's chosen type or help.",
668 ),
669 collisions_gauge: gauge(
670 "mz_compute_metric_sink_collisions",
671 "The number of series with more than one distinct live value for the same metric name and labels.",
672 ),
673 null_values_gauge: gauge(
674 "mz_compute_metric_sink_null_values",
675 "The number of series currently suppressed because their value is null.",
676 ),
677 }
678 }
679}
680
681impl Collector for SinkCollector {
682 fn desc(&self) -> Vec<&Desc> {
683 let mut descs = Vec::with_capacity(6);
684 descs.extend(self.frontier_gauge.desc());
685 descs.extend(self.errors_gauge.desc());
686 descs.extend(self.skipped_gauge.desc());
687 descs.extend(self.conflicts_gauge.desc());
688 descs.extend(self.collisions_gauge.desc());
689 descs.extend(self.null_values_gauge.desc());
690 descs
691 }
692
693 fn collect(&self) -> Vec<MetricFamily> {
694 let mut families = {
695 let state = self.state.lock().expect("sink state mutex poisoned");
696 self.frontier_gauge.set(f64::cast_lossy(state.frontier_ms));
697 self.errors_gauge.set(f64::cast_lossy(state.errors));
698 self.skipped_gauge.set(f64::cast_lossy(state.skipped));
699 self.conflicts_gauge.set(f64::cast_lossy(state.conflicts));
700 self.collisions_gauge.set(f64::cast_lossy(state.collisions));
701 self.null_values_gauge
702 .set(f64::cast_lossy(state.null_values));
703 build_families(&state.published)
704 };
705
706 families.extend(self.frontier_gauge.collect());
707 families.extend(self.errors_gauge.collect());
708 families.extend(self.skipped_gauge.collect());
709 families.extend(self.conflicts_gauge.collect());
710 families.extend(self.collisions_gauge.collect());
711 families.extend(self.null_values_gauge.collect());
712 families
713 }
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719
720 fn frontier(bound: u64) -> Antichain<Timestamp> {
722 Antichain::from_elem(Timestamp::from(bound))
723 }
724
725 fn label_a() -> Vec<(String, String)> {
726 vec![("a".into(), "1".into())]
727 }
728
729 const LABEL_A: &[(&str, &str)] = &[("a", "1")];
731
732 fn key_m() -> PublishedKey {
733 ("m".into(), label_a())
734 }
735
736 fn stage_m(st: &mut SinkState, value: f64, time: u64, diff: i64) {
738 st.stage_ok(
739 "m",
740 Some(MetricKind::Gauge),
741 true,
742 LABEL_A,
743 Some(value),
744 "h",
745 Timestamp::from(time),
746 diff,
747 );
748 }
749
750 fn stage_m_null(st: &mut SinkState, time: u64, diff: i64) {
752 st.stage_ok(
753 "m",
754 Some(MetricKind::Gauge),
755 true,
756 LABEL_A,
757 None,
758 "h",
759 Timestamp::from(time),
760 diff,
761 );
762 }
763
764 #[mz_ore::test]
765 fn fold_and_publish() {
766 let mut st = SinkState::default();
767 stage_m(&mut st, 2.0, 0, 1);
768 st.integrate(&frontier(1));
769 st.publish_if_healthy();
770 assert_eq!(st.published.len(), 1);
771 assert_eq!(st.published[&key_m()].0, 2.0);
772
773 st.stage_ok("h1", None, true, &[], Some(1.0), "h", Timestamp::from(1), 1);
775 st.integrate(&frontier(2));
776 assert_eq!(st.skipped, 1);
777
778 st.errors = 1;
781 stage_m(&mut st, 2.0, 2, -1);
782 stage_m(&mut st, 9.0, 2, 1);
783 st.integrate(&frontier(3));
784 st.publish_if_healthy();
785 assert_eq!(st.published[&key_m()].0, 2.0);
786
787 st.errors = 0;
789 st.publish_if_healthy();
790 assert_eq!(st.published[&key_m()].0, 9.0);
791 assert_eq!(st.collisions, 0);
792 }
793
794 #[mz_ore::test]
795 fn value_update_split_across_activations_no_collision() {
796 let mut st = SinkState::default();
797 stage_m(&mut st, 5.0, 0, 1);
799 st.integrate(&frontier(1));
800 st.publish_if_healthy();
801 assert_eq!(st.published[&key_m()].0, 5.0);
802
803 stage_m(&mut st, 9.0, 1, 1);
806 st.integrate(&frontier(1));
807 st.publish_if_healthy();
808 assert_eq!(st.collisions, 0);
809 stage_m(&mut st, 5.0, 1, -1);
810
811 st.integrate(&frontier(2));
813 st.publish_if_healthy();
814 assert_eq!(st.published[&key_m()].0, 9.0);
815 assert_eq!(st.collisions, 0);
816 }
817
818 #[mz_ore::test]
819 fn duplicate_multiplicity_consolidates() {
820 let mut st = SinkState::default();
821 stage_m(&mut st, 5.0, 0, 1);
823 stage_m(&mut st, 5.0, 0, 1);
824 st.integrate(&frontier(1));
825 st.publish_if_healthy();
826 assert_eq!(st.published[&key_m()].0, 5.0);
827 assert_eq!(st.collisions, 0);
828
829 stage_m(&mut st, 7.0, 1, 1);
831 st.integrate(&frontier(2));
832 st.publish_if_healthy();
833 assert_eq!(st.collisions, 1);
834 assert_eq!(st.published[&key_m()].0, 5.0);
836 }
837
838 #[mz_ore::test]
839 fn no_publish_before_time_closed() {
840 let mut st = SinkState::default();
841 stage_m(&mut st, 2.0, 5, 1);
843 st.integrate(&frontier(5));
844 st.publish_if_healthy();
845 assert!(st.published.is_empty());
846
847 st.integrate(&frontier(6));
849 st.publish_if_healthy();
850 assert_eq!(st.published[&key_m()].0, 2.0);
851 }
852
853 #[mz_ore::test]
854 fn null_value_gaps_series() {
855 let mut st = SinkState::default();
856 stage_m_null(&mut st, 1, 1);
858 st.integrate(&frontier(2));
859 st.publish_if_healthy();
860 assert!(!st.published.contains_key(&key_m()));
861 assert_eq!(st.null_values, 1);
862
863 stage_m(&mut st, 5.0, 3, 1);
866 st.integrate(&frontier(4));
867 st.publish_if_healthy();
868 assert_eq!(st.published[&key_m()].0, 5.0);
869 assert_eq!(st.null_values, 0);
870 }
871
872 #[mz_ore::test]
873 fn null_labels_become_empty() {
874 let mut st = SinkState::default();
875 st.stage_ok(
878 "m",
879 Some(MetricKind::Gauge),
880 true,
881 &[],
882 Some(1.0),
883 "h",
884 Timestamp::from(1),
885 1,
886 );
887 st.integrate(&frontier(2));
888 st.publish_if_healthy();
889 assert_eq!(st.published[&("m".into(), vec![])].0, 1.0);
890 }
891
892 #[mz_ore::test]
893 fn extract_row_normalizes_null_datums() {
894 use mz_repr::SqlScalarType;
895
896 let desc = RelationDesc::builder()
900 .with_column("metric_name", SqlScalarType::String.nullable(true))
901 .with_column(
902 "labels",
903 SqlScalarType::Map {
904 value_type: Box::new(SqlScalarType::String),
905 custom_id: None,
906 }
907 .nullable(false),
908 )
909 .with_column("value", SqlScalarType::Float64.nullable(true))
910 .with_column("help", SqlScalarType::String.nullable(false))
911 .with_column("metric_kind", SqlScalarType::Int32.nullable(true))
912 .with_column("name_valid", SqlScalarType::Bool.nullable(true))
913 .finish();
914 let cols = ColumnIndices::resolve(&desc);
915
916 let mut row = Row::default();
917 {
918 let mut packer = row.packer();
919 packer.push(Datum::Null); packer.push_dict_with(|_| {}); packer.push(Datum::Null); packer.push(Datum::String("")); packer.push(Datum::Null); packer.push(Datum::Null); }
926
927 let datums: Vec<Datum> = row.iter().collect();
928 let (name, metric_kind, name_valid, labels, value, help) = extract_row(&cols, &datums);
929 assert_eq!(name, "");
930 assert_eq!(metric_kind, None);
931 assert!(!name_valid);
932 assert_eq!(labels, Vec::<(&str, &str)>::new());
933 assert_eq!(value, None);
934 assert_eq!(help, "");
935 }
936
937 fn pkey(name: &str, labels: &[(&str, &str)]) -> PublishedKey {
938 (
939 name.to_string(),
940 labels
941 .iter()
942 .map(|&(k, v)| (k.to_string(), v.to_string()))
943 .collect(),
944 )
945 }
946
947 #[mz_ore::test]
948 fn build_families_groups_by_name_and_kind() {
949 let published: BTreeMap<PublishedKey, PublishedValue> = BTreeMap::from([
950 (
951 pkey("http_requests", &[("code", "200")]),
952 (5.0, MetricKind::Counter, "requests".to_string()),
953 ),
954 (
955 pkey("http_requests", &[("code", "500")]),
956 (2.0, MetricKind::Counter, "requests".to_string()),
957 ),
958 (
959 pkey("temp_celsius", &[]),
960 (21.5, MetricKind::Gauge, "temperature".to_string()),
961 ),
962 ]);
963
964 let families = build_families(&published);
965
966 assert_eq!(families.len(), 2);
968
969 let requests = &families[0];
970 assert_eq!(requests.name(), "http_requests");
971 assert_eq!(requests.help(), "requests");
972 let metrics = requests.get_metric();
973 assert_eq!(metrics.len(), 2);
974 assert_eq!(metrics[0].get_label()[0].value(), "200");
976 assert_eq!(metrics[0].get_counter().value(), 5.0);
977 assert_eq!(metrics[1].get_label()[0].value(), "500");
978 assert_eq!(metrics[1].get_counter().value(), 2.0);
979
980 let temp = &families[1];
981 assert_eq!(temp.name(), "temp_celsius");
982 let temp_metrics = temp.get_metric();
983 assert_eq!(temp_metrics.len(), 1);
984 assert_eq!(temp_metrics[0].get_gauge().value(), 21.5);
985 }
986
987 #[mz_ore::test]
988 fn count_conflicts_flags_type_and_help_disagreement() {
989 let published: BTreeMap<PublishedKey, PublishedValue> = BTreeMap::from([
992 (
993 pkey("m", &[("a", "1")]),
994 (1.0, MetricKind::Gauge, "h1".to_string()),
995 ),
996 (
997 pkey("m", &[("b", "2")]),
998 (2.0, MetricKind::Counter, "h1".to_string()),
999 ),
1000 (
1001 pkey("m", &[("c", "3")]),
1002 (3.0, MetricKind::Gauge, "h2".to_string()),
1003 ),
1004 (
1005 pkey("other", &[]),
1006 (1.0, MetricKind::Gauge, "h".to_string()),
1007 ),
1008 ]);
1009 assert_eq!(count_conflicts(&published), 2);
1010
1011 let consistent: BTreeMap<PublishedKey, PublishedValue> = BTreeMap::from([
1013 (
1014 pkey("m", &[("a", "1")]),
1015 (1.0, MetricKind::Gauge, "h".to_string()),
1016 ),
1017 (
1018 pkey("m", &[("b", "2")]),
1019 (2.0, MetricKind::Gauge, "h".to_string()),
1020 ),
1021 ]);
1022 assert_eq!(count_conflicts(&consistent), 0);
1023 }
1024
1025 #[mz_ore::test]
1026 fn err_stream_freezes_and_recovers() {
1027 let mut st = SinkState::default();
1028 stage_m(&mut st, 5.0, 0, 1);
1030 st.integrate(&frontier(1));
1031 st.publish_if_healthy();
1032 assert_eq!(st.published[&key_m()].0, 5.0);
1033
1034 st.stage_err(Timestamp::from(1), 1);
1037 stage_m(&mut st, 5.0, 1, -1);
1038 stage_m(&mut st, 9.0, 1, 1);
1039 st.integrate(&frontier(2));
1040 assert_eq!(st.errors, 1);
1041 st.publish_if_healthy();
1042 assert_eq!(st.published[&key_m()].0, 5.0);
1044
1045 st.stage_err(Timestamp::from(2), -1);
1048 st.integrate(&frontier(3));
1049 assert_eq!(st.errors, 0);
1050 st.publish_if_healthy();
1051 assert_eq!(st.published[&key_m()].0, 9.0);
1052 }
1053}