1use std::any::Any;
27use std::collections::BTreeMap;
28use std::rc::Rc;
29use std::sync::{Arc, Mutex};
30
31use differential_dataflow::{Hashable, VecCollection};
32use mz_compute_types::sinks::{ComputeSinkDesc, MetricSinkConnection};
33use mz_ore::cast::{CastFrom, CastLossy};
34use mz_repr::{ColumnName, Datum, DatumVec, Diff, GlobalId, RelationDesc, Row, Timestamp};
35use mz_storage_types::controller::CollectionMetadata;
36use mz_timely_util::probe::{Handle, ProbeNotify};
37use prometheus::core::{Collector, Desc};
38use prometheus::proto::{
39 Counter as ProtoCounter, Gauge as ProtoGauge, LabelPair, Metric as ProtoMetric, MetricFamily,
40 MetricType,
41};
42use prometheus::{Gauge, Opts};
43use timely::dataflow::channels::pact::Exchange;
44use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
45use timely::progress::Antichain;
46
47use crate::render::StartSignal;
48use crate::render::errors::DataflowErrorSer;
49use crate::render::sinks::SinkRender;
50
51impl<'scope> SinkRender<'scope> for MetricSinkConnection {
52 fn render_sink(
53 &self,
54 compute_state: &mut crate::compute_state::ComputeState,
55 sink: &ComputeSinkDesc<CollectionMetadata>,
56 sink_id: GlobalId,
57 _as_of: Antichain<Timestamp>,
58 _start_signal: StartSignal,
59 sinked_collection: VecCollection<'scope, Timestamp, Row, Diff>,
60 err_collection: VecCollection<'scope, Timestamp, DataflowErrorSer, Diff>,
61 output_probe: &Handle<Timestamp>,
62 ) -> Option<Rc<dyn Any>> {
63 let cols = ColumnIndices::resolve(&sink.from_desc);
64
65 let scope = sinked_collection.scope();
66 let worker_id = scope.index();
67 let active_worker_id = usize::cast_from(sink_id.hashed()) % scope.peers();
75
76 let ok_stream = sinked_collection
77 .inner
78 .probe_notify_with(vec![output_probe.clone()]);
79 let err_stream = err_collection.inner;
80
81 let state = Arc::new(Mutex::new(SinkState::default()));
82
83 let drop_handle = (worker_id == active_worker_id).then(|| {
95 let collector = SinkCollector::new(sink_id, Arc::clone(&state));
96 compute_state
97 .metrics_registry
98 .register_collector_with_dropper(collector)
99 });
100
101 let mut op = OperatorBuilder::new(format!("MetricSink({sink_id})"), scope);
102 let mut ok_input = op.new_input(
103 ok_stream,
104 Exchange::new(move |_: &(Row, Timestamp, Diff)| u64::cast_from(active_worker_id)),
105 );
106 let mut err_input = op.new_input(
107 err_stream,
108 Exchange::new(move |_: &(DataflowErrorSer, Timestamp, Diff)| {
109 u64::cast_from(active_worker_id)
110 }),
111 );
112
113 op.build(move |_capabilities| {
114 let mut datum_vec = DatumVec::new();
117 move |frontiers| {
118 if worker_id != active_worker_id {
119 ok_input.for_each(|_, _| {});
122 err_input.for_each(|_, _| {});
123 return;
124 }
125
126 let mut st = state.lock().expect("sink state mutex poisoned");
127
128 ok_input.for_each(|_, data| {
133 for (row, time, diff) in data.drain(..) {
134 let datums = datum_vec.borrow_with(&row);
135 let (name, metric_kind, name_valid, labels, value, help) =
136 extract_row(&cols, &datums);
137 st.stage_ok(
138 name,
139 metric_kind,
140 name_valid,
141 &labels,
142 value,
143 help,
144 time,
145 diff.into_inner(),
146 );
147 }
148 });
149 err_input.for_each(|_, data| {
150 for (_err, time, diff) in data.drain(..) {
151 st.stage_err(time, diff.into_inner());
152 }
153 });
154
155 let mut frontier = Antichain::new();
158 for f in frontiers {
159 frontier.extend(f.frontier().iter().copied());
160 }
161
162 st.integrate(&frontier);
163 st.frontier_ms = frontier
164 .as_option()
165 .map(|t| u64::from(*t))
166 .unwrap_or(u64::MAX);
167 st.publish_if_healthy();
168 }
169 });
170
171 Some(Rc::new(drop_handle))
172 }
173}
174
175struct ColumnIndices {
183 metric_name: usize,
184 labels: usize,
185 value: usize,
186 help: usize,
187 metric_kind: usize,
188 name_valid: usize,
189}
190
191impl ColumnIndices {
192 fn resolve(desc: &RelationDesc) -> Self {
193 let idx = |name: &str| {
194 desc.get_by_name(&ColumnName::from(name))
195 .expect("column existence validated by the SQL planner")
196 .0
197 };
198 ColumnIndices {
199 metric_name: idx("metric_name"),
200 labels: idx("labels"),
201 value: idx("value"),
202 help: idx("help"),
203 metric_kind: idx("metric_kind"),
204 name_valid: idx("name_valid"),
205 }
206 }
207}
208
209fn extract_row<'a>(
217 cols: &ColumnIndices,
218 datums: &[Datum<'a>],
219) -> (
220 &'a str,
221 Option<MetricKind>,
222 bool,
223 Vec<(&'a str, &'a str)>,
224 Option<f64>,
225 &'a str,
226) {
227 let metric_name = match datums[cols.metric_name] {
228 Datum::Null => "",
229 d => d.unwrap_str(),
230 };
231 let metric_kind = MetricKind::from_datum(datums[cols.metric_kind]);
232 let name_valid = matches!(datums[cols.name_valid], Datum::True);
233 let mut labels: Vec<(&str, &str)> = datums[cols.labels]
234 .unwrap_map()
235 .iter()
236 .map(|(k, v)| (k, v.unwrap_str()))
237 .collect();
238 labels.sort();
239 let value = match datums[cols.value] {
240 Datum::Null => None,
241 d => Some(d.unwrap_float64()),
242 };
243 let help = datums[cols.help].unwrap_str();
244 (metric_name, metric_kind, name_valid, labels, value, help)
245}
246
247type RowKey = (
261 String,
262 Vec<(String, String)>,
263 Option<u64>,
264 Option<MetricKind>,
265 bool,
266 String,
267);
268
269type PublishedKey = (String, Vec<(String, String)>);
271type PublishedValue = (f64, MetricKind, String);
273
274#[derive(Default)]
285struct SinkState {
286 pending_ok: BTreeMap<Timestamp, BTreeMap<RowKey, i64>>,
288 pending_err: BTreeMap<Timestamp, i64>,
290 working: BTreeMap<RowKey, i64>,
292 published: BTreeMap<PublishedKey, PublishedValue>,
293 errors: i64,
296 frontier_ms: u64,
297 skipped: u64,
298 conflicts: u64,
299 collisions: u64,
300 null_values: u64,
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
307enum MetricKind {
308 Gauge,
309 Counter,
310}
311
312impl MetricKind {
313 fn from_datum(d: Datum) -> Option<Self> {
316 match d {
317 Datum::Int32(0) => Some(MetricKind::Gauge),
318 Datum::Int32(1) => Some(MetricKind::Counter),
319 _ => None,
320 }
321 }
322
323 fn proto_type(self) -> MetricType {
324 match self {
325 MetricKind::Gauge => MetricType::GAUGE,
326 MetricKind::Counter => MetricType::COUNTER,
327 }
328 }
329}
330
331fn is_valid_label_name(name: &str) -> bool {
337 let mut chars = name.chars();
338 match chars.next() {
339 Some(c) if c.is_ascii_alphabetic() || c == '_' => {
340 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
341 }
342 _ => false,
343 }
344}
345
346impl SinkState {
347 fn stage_ok(
354 &mut self,
355 metric_name: &str,
356 metric_kind: Option<MetricKind>,
357 name_valid: bool,
358 labels: &[(&str, &str)],
359 value: Option<f64>,
360 help: &str,
361 time: Timestamp,
362 diff: i64,
363 ) {
364 let key = (
365 metric_name.to_string(),
366 labels
367 .iter()
368 .map(|&(k, v)| (k.to_string(), v.to_string()))
369 .collect(),
370 value.map(f64::to_bits),
371 metric_kind,
372 name_valid,
373 help.to_string(),
374 );
375 *self
376 .pending_ok
377 .entry(time)
378 .or_default()
379 .entry(key)
380 .or_default() += diff;
381 }
382
383 fn stage_err(&mut self, time: Timestamp, diff: i64) {
385 *self.pending_err.entry(time).or_default() += diff;
386 }
387
388 fn integrate(&mut self, frontier: &Antichain<Timestamp>) {
395 let closed_ok: Vec<Timestamp> = self
396 .pending_ok
397 .keys()
398 .filter(|t| !frontier.less_equal(t))
399 .copied()
400 .collect();
401 for time in closed_ok {
402 let rows = self.pending_ok.remove(&time).expect("key from keys()");
403 for (key, diff) in rows {
404 *self.working.entry(key).or_default() += diff;
405 }
406 }
407
408 let closed_err: Vec<Timestamp> = self
409 .pending_err
410 .keys()
411 .filter(|t| !frontier.less_equal(t))
412 .copied()
413 .collect();
414 for time in closed_err {
415 self.errors += self.pending_err.remove(&time).expect("key from keys()");
416 }
417
418 self.working.retain(|_, acc| *acc != 0);
419 self.skipped = count_skipped(&self.working);
420 }
421
422 fn publish_if_healthy(&mut self) {
432 if self.errors == 0 {
433 let (published, collisions, null_values) = rebuild_published(&self.working);
434 self.published = published;
435 self.collisions = collisions;
436 self.null_values = null_values;
437 self.conflicts = count_conflicts(&self.published);
438 }
439 }
440}
441
442fn count_skipped(working: &BTreeMap<RowKey, i64>) -> u64 {
445 let mut skipped = 0u64;
446 for ((_name, labels, _bits, metric_kind, name_valid, _help), acc) in working {
447 if *acc <= 0 {
448 continue;
449 }
450 let unsupported = metric_kind.is_none();
451 let invalid = !name_valid || !labels.iter().all(|(k, _)| is_valid_label_name(k));
452 if unsupported || invalid {
453 skipped += 1;
454 }
455 }
456 skipped
457}
458
459fn rebuild_published(
472 working: &BTreeMap<RowKey, i64>,
473) -> (BTreeMap<PublishedKey, PublishedValue>, u64, u64) {
474 let mut grouped: BTreeMap<PublishedKey, Vec<(Option<f64>, MetricKind, String)>> =
476 BTreeMap::new();
477 for ((name, labels, bits, metric_kind, name_valid, help), acc) in working {
478 if *acc <= 0 {
479 continue;
480 }
481 let Some(kind) = metric_kind else {
482 continue;
483 };
484 if !name_valid || !labels.iter().all(|(k, _)| is_valid_label_name(k)) {
485 continue;
486 }
487 grouped
488 .entry((name.clone(), labels.clone()))
489 .or_default()
490 .push((bits.map(f64::from_bits), *kind, help.clone()));
491 }
492
493 let mut published = BTreeMap::new();
494 let mut collisions = 0u64;
495 let mut null_values = 0u64;
496 for (key, candidates) in grouped {
497 let mut non_null: Vec<PublishedValue> = candidates
498 .into_iter()
499 .filter_map(|(value, kind, help)| value.map(|v| (v, kind, help)))
500 .collect();
501 if non_null.is_empty() {
502 null_values += 1;
503 continue;
504 }
505 let mut distinct: Vec<u64> = non_null.iter().map(|(v, _, _)| v.to_bits()).collect();
506 distinct.sort_unstable();
507 distinct.dedup();
508 if distinct.len() > 1 {
509 collisions += 1;
510 }
511 non_null.sort_by(|a, b| {
512 a.0.total_cmp(&b.0)
513 .then(a.1.cmp(&b.1))
514 .then_with(|| a.2.cmp(&b.2))
515 });
516 let winner = non_null
517 .into_iter()
518 .next()
519 .expect("checked non-empty above");
520 published.insert(key, winner);
521 }
522 (published, collisions, null_values)
523}
524
525fn count_conflicts(published: &BTreeMap<PublishedKey, PublishedValue>) -> u64 {
528 let mut conflicts = 0u64;
529 let mut winner: Option<(&str, MetricKind, &str)> = None;
530 for ((name, _labels), (_value, kind, help)) in published {
531 winner = match winner {
532 Some((n, k, h)) if n == name.as_str() => {
533 if k != *kind || h != help.as_str() {
534 conflicts += 1;
535 }
536 Some((n, k, h))
537 }
538 _ => Some((name.as_str(), *kind, help.as_str())),
539 };
540 }
541 conflicts
542}
543
544fn build_families(published: &BTreeMap<PublishedKey, PublishedValue>) -> Vec<MetricFamily> {
553 let mut families = Vec::new();
554 let mut group_name: Option<&str> = None;
555 let mut family: Option<MetricFamily> = None;
556 let mut family_kind = MetricKind::Gauge;
557
558 for ((name, labels), (value, kind, help)) in published {
559 if group_name != Some(name.as_str()) {
560 if let Some(f) = family.take() {
561 families.push(f);
562 }
563 let mut mf = MetricFamily::new();
564 mf.name = Some(name.clone());
565 mf.help = Some(help.clone());
566 mf.type_ = Some(kind.proto_type().into());
567 family = Some(mf);
568 family_kind = *kind;
569 group_name = Some(name.as_str());
570 }
571
572 let mut metric = ProtoMetric::new();
573 metric.label = labels
574 .iter()
575 .map(|(k, v)| {
576 let mut lp = LabelPair::new();
577 lp.name = Some(k.clone());
578 lp.value = Some(v.clone());
579 lp
580 })
581 .collect();
582 match family_kind {
583 MetricKind::Gauge => {
584 let mut g = ProtoGauge::new();
585 g.value = Some(*value);
586 metric.gauge = Some(g).into();
587 }
588 MetricKind::Counter => {
589 let mut c = ProtoCounter::new();
590 c.value = Some(*value);
591 metric.counter = Some(c).into();
592 }
593 }
594 family
595 .as_mut()
596 .expect("initialized above for the first entry of every group")
597 .metric
598 .push(metric);
599 }
600 if let Some(f) = family.take() {
601 families.push(f);
602 }
603 families
604}
605
606#[derive(Clone)]
615struct SinkCollector {
616 state: Arc<Mutex<SinkState>>,
617 frontier_gauge: Gauge,
618 errors_gauge: Gauge,
619 skipped_gauge: Gauge,
620 conflicts_gauge: Gauge,
621 collisions_gauge: Gauge,
622 null_values_gauge: Gauge,
623}
624
625impl SinkCollector {
626 fn new(sink_id: GlobalId, state: Arc<Mutex<SinkState>>) -> Self {
627 let gauge = |name: &str, help: &str| {
628 Gauge::with_opts(Opts::new(name, help).const_label("sink", sink_id.to_string()))
629 .expect("static metric sink companion gauge options are valid")
630 };
631 SinkCollector {
632 state,
633 frontier_gauge: gauge(
634 "mz_metric_sink_frontier_ms",
635 "The metric sink's input frontier, in milliseconds since the epoch.",
636 ),
637 errors_gauge: gauge(
638 "mz_metric_sink_errors",
639 "The number of live errors on the metric sink's input.",
640 ),
641 skipped_gauge: gauge(
642 "mz_metric_sink_skipped",
643 "The number of input rows skipped for an unsupported metric type or an invalid name.",
644 ),
645 conflicts_gauge: gauge(
646 "mz_metric_sink_conflicts",
647 "The number of published series whose type or help disagree with their family's chosen type or help.",
648 ),
649 collisions_gauge: gauge(
650 "mz_metric_sink_collisions",
651 "The number of series with more than one distinct live value for the same metric name and labels.",
652 ),
653 null_values_gauge: gauge(
654 "mz_metric_sink_null_values",
655 "The number of series currently suppressed because their value is null.",
656 ),
657 }
658 }
659}
660
661impl Collector for SinkCollector {
662 fn desc(&self) -> Vec<&Desc> {
663 let mut descs = Vec::with_capacity(6);
664 descs.extend(self.frontier_gauge.desc());
665 descs.extend(self.errors_gauge.desc());
666 descs.extend(self.skipped_gauge.desc());
667 descs.extend(self.conflicts_gauge.desc());
668 descs.extend(self.collisions_gauge.desc());
669 descs.extend(self.null_values_gauge.desc());
670 descs
671 }
672
673 fn collect(&self) -> Vec<MetricFamily> {
674 let mut families = {
675 let state = self.state.lock().expect("sink state mutex poisoned");
676 self.frontier_gauge.set(f64::cast_lossy(state.frontier_ms));
677 self.errors_gauge.set(f64::cast_lossy(state.errors));
678 self.skipped_gauge.set(f64::cast_lossy(state.skipped));
679 self.conflicts_gauge.set(f64::cast_lossy(state.conflicts));
680 self.collisions_gauge.set(f64::cast_lossy(state.collisions));
681 self.null_values_gauge
682 .set(f64::cast_lossy(state.null_values));
683 build_families(&state.published)
684 };
685
686 families.extend(self.frontier_gauge.collect());
687 families.extend(self.errors_gauge.collect());
688 families.extend(self.skipped_gauge.collect());
689 families.extend(self.conflicts_gauge.collect());
690 families.extend(self.collisions_gauge.collect());
691 families.extend(self.null_values_gauge.collect());
692 families
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699
700 fn frontier(bound: u64) -> Antichain<Timestamp> {
702 Antichain::from_elem(Timestamp::from(bound))
703 }
704
705 fn label_a() -> Vec<(String, String)> {
706 vec![("a".into(), "1".into())]
707 }
708
709 const LABEL_A: &[(&str, &str)] = &[("a", "1")];
711
712 fn key_m() -> PublishedKey {
713 ("m".into(), label_a())
714 }
715
716 fn stage_m(st: &mut SinkState, value: f64, time: u64, diff: i64) {
718 st.stage_ok(
719 "m",
720 Some(MetricKind::Gauge),
721 true,
722 LABEL_A,
723 Some(value),
724 "h",
725 Timestamp::from(time),
726 diff,
727 );
728 }
729
730 fn stage_m_null(st: &mut SinkState, time: u64, diff: i64) {
732 st.stage_ok(
733 "m",
734 Some(MetricKind::Gauge),
735 true,
736 LABEL_A,
737 None,
738 "h",
739 Timestamp::from(time),
740 diff,
741 );
742 }
743
744 #[mz_ore::test]
745 fn fold_and_publish() {
746 let mut st = SinkState::default();
747 stage_m(&mut st, 2.0, 0, 1);
748 st.integrate(&frontier(1));
749 st.publish_if_healthy();
750 assert_eq!(st.published.len(), 1);
751 assert_eq!(st.published[&key_m()].0, 2.0);
752
753 st.stage_ok("h1", None, true, &[], Some(1.0), "h", Timestamp::from(1), 1);
755 st.integrate(&frontier(2));
756 assert_eq!(st.skipped, 1);
757
758 st.errors = 1;
761 stage_m(&mut st, 2.0, 2, -1);
762 stage_m(&mut st, 9.0, 2, 1);
763 st.integrate(&frontier(3));
764 st.publish_if_healthy();
765 assert_eq!(st.published[&key_m()].0, 2.0);
766
767 st.errors = 0;
769 st.publish_if_healthy();
770 assert_eq!(st.published[&key_m()].0, 9.0);
771 assert_eq!(st.collisions, 0);
772 }
773
774 #[mz_ore::test]
775 fn value_update_split_across_activations_no_collision() {
776 let mut st = SinkState::default();
777 stage_m(&mut st, 5.0, 0, 1);
779 st.integrate(&frontier(1));
780 st.publish_if_healthy();
781 assert_eq!(st.published[&key_m()].0, 5.0);
782
783 stage_m(&mut st, 9.0, 1, 1);
786 st.integrate(&frontier(1));
787 st.publish_if_healthy();
788 assert_eq!(st.collisions, 0);
789 stage_m(&mut st, 5.0, 1, -1);
790
791 st.integrate(&frontier(2));
793 st.publish_if_healthy();
794 assert_eq!(st.published[&key_m()].0, 9.0);
795 assert_eq!(st.collisions, 0);
796 }
797
798 #[mz_ore::test]
799 fn duplicate_multiplicity_consolidates() {
800 let mut st = SinkState::default();
801 stage_m(&mut st, 5.0, 0, 1);
803 stage_m(&mut st, 5.0, 0, 1);
804 st.integrate(&frontier(1));
805 st.publish_if_healthy();
806 assert_eq!(st.published[&key_m()].0, 5.0);
807 assert_eq!(st.collisions, 0);
808
809 stage_m(&mut st, 7.0, 1, 1);
811 st.integrate(&frontier(2));
812 st.publish_if_healthy();
813 assert_eq!(st.collisions, 1);
814 assert_eq!(st.published[&key_m()].0, 5.0);
816 }
817
818 #[mz_ore::test]
819 fn no_publish_before_time_closed() {
820 let mut st = SinkState::default();
821 stage_m(&mut st, 2.0, 5, 1);
823 st.integrate(&frontier(5));
824 st.publish_if_healthy();
825 assert!(st.published.is_empty());
826
827 st.integrate(&frontier(6));
829 st.publish_if_healthy();
830 assert_eq!(st.published[&key_m()].0, 2.0);
831 }
832
833 #[mz_ore::test]
834 fn null_value_gaps_series() {
835 let mut st = SinkState::default();
836 stage_m_null(&mut st, 1, 1);
838 st.integrate(&frontier(2));
839 st.publish_if_healthy();
840 assert!(!st.published.contains_key(&key_m()));
841 assert_eq!(st.null_values, 1);
842
843 stage_m(&mut st, 5.0, 3, 1);
846 st.integrate(&frontier(4));
847 st.publish_if_healthy();
848 assert_eq!(st.published[&key_m()].0, 5.0);
849 assert_eq!(st.null_values, 0);
850 }
851
852 #[mz_ore::test]
853 fn null_labels_become_empty() {
854 let mut st = SinkState::default();
855 st.stage_ok(
858 "m",
859 Some(MetricKind::Gauge),
860 true,
861 &[],
862 Some(1.0),
863 "h",
864 Timestamp::from(1),
865 1,
866 );
867 st.integrate(&frontier(2));
868 st.publish_if_healthy();
869 assert_eq!(st.published[&("m".into(), vec![])].0, 1.0);
870 }
871
872 #[mz_ore::test]
873 fn extract_row_normalizes_null_datums() {
874 use mz_repr::SqlScalarType;
875
876 let desc = RelationDesc::builder()
880 .with_column("metric_name", SqlScalarType::String.nullable(true))
881 .with_column(
882 "labels",
883 SqlScalarType::Map {
884 value_type: Box::new(SqlScalarType::String),
885 custom_id: None,
886 }
887 .nullable(false),
888 )
889 .with_column("value", SqlScalarType::Float64.nullable(true))
890 .with_column("help", SqlScalarType::String.nullable(false))
891 .with_column("metric_kind", SqlScalarType::Int32.nullable(true))
892 .with_column("name_valid", SqlScalarType::Bool.nullable(true))
893 .finish();
894 let cols = ColumnIndices::resolve(&desc);
895
896 let mut row = Row::default();
897 {
898 let mut packer = row.packer();
899 packer.push(Datum::Null); packer.push_dict_with(|_| {}); packer.push(Datum::Null); packer.push(Datum::String("")); packer.push(Datum::Null); packer.push(Datum::Null); }
906
907 let datums: Vec<Datum> = row.iter().collect();
908 let (name, metric_kind, name_valid, labels, value, help) = extract_row(&cols, &datums);
909 assert_eq!(name, "");
910 assert_eq!(metric_kind, None);
911 assert!(!name_valid);
912 assert_eq!(labels, Vec::<(&str, &str)>::new());
913 assert_eq!(value, None);
914 assert_eq!(help, "");
915 }
916
917 fn pkey(name: &str, labels: &[(&str, &str)]) -> PublishedKey {
918 (
919 name.to_string(),
920 labels
921 .iter()
922 .map(|&(k, v)| (k.to_string(), v.to_string()))
923 .collect(),
924 )
925 }
926
927 #[mz_ore::test]
928 fn build_families_groups_by_name_and_kind() {
929 let published: BTreeMap<PublishedKey, PublishedValue> = BTreeMap::from([
930 (
931 pkey("http_requests", &[("code", "200")]),
932 (5.0, MetricKind::Counter, "requests".to_string()),
933 ),
934 (
935 pkey("http_requests", &[("code", "500")]),
936 (2.0, MetricKind::Counter, "requests".to_string()),
937 ),
938 (
939 pkey("temp_celsius", &[]),
940 (21.5, MetricKind::Gauge, "temperature".to_string()),
941 ),
942 ]);
943
944 let families = build_families(&published);
945
946 assert_eq!(families.len(), 2);
948
949 let requests = &families[0];
950 assert_eq!(requests.name(), "http_requests");
951 assert_eq!(requests.help(), "requests");
952 let metrics = requests.get_metric();
953 assert_eq!(metrics.len(), 2);
954 assert_eq!(metrics[0].get_label()[0].value(), "200");
956 assert_eq!(metrics[0].get_counter().value(), 5.0);
957 assert_eq!(metrics[1].get_label()[0].value(), "500");
958 assert_eq!(metrics[1].get_counter().value(), 2.0);
959
960 let temp = &families[1];
961 assert_eq!(temp.name(), "temp_celsius");
962 let temp_metrics = temp.get_metric();
963 assert_eq!(temp_metrics.len(), 1);
964 assert_eq!(temp_metrics[0].get_gauge().value(), 21.5);
965 }
966
967 #[mz_ore::test]
968 fn count_conflicts_flags_type_and_help_disagreement() {
969 let published: BTreeMap<PublishedKey, PublishedValue> = BTreeMap::from([
972 (
973 pkey("m", &[("a", "1")]),
974 (1.0, MetricKind::Gauge, "h1".to_string()),
975 ),
976 (
977 pkey("m", &[("b", "2")]),
978 (2.0, MetricKind::Counter, "h1".to_string()),
979 ),
980 (
981 pkey("m", &[("c", "3")]),
982 (3.0, MetricKind::Gauge, "h2".to_string()),
983 ),
984 (
985 pkey("other", &[]),
986 (1.0, MetricKind::Gauge, "h".to_string()),
987 ),
988 ]);
989 assert_eq!(count_conflicts(&published), 2);
990
991 let consistent: BTreeMap<PublishedKey, PublishedValue> = BTreeMap::from([
993 (
994 pkey("m", &[("a", "1")]),
995 (1.0, MetricKind::Gauge, "h".to_string()),
996 ),
997 (
998 pkey("m", &[("b", "2")]),
999 (2.0, MetricKind::Gauge, "h".to_string()),
1000 ),
1001 ]);
1002 assert_eq!(count_conflicts(&consistent), 0);
1003 }
1004
1005 #[mz_ore::test]
1006 fn err_stream_freezes_and_recovers() {
1007 let mut st = SinkState::default();
1008 stage_m(&mut st, 5.0, 0, 1);
1010 st.integrate(&frontier(1));
1011 st.publish_if_healthy();
1012 assert_eq!(st.published[&key_m()].0, 5.0);
1013
1014 st.stage_err(Timestamp::from(1), 1);
1017 stage_m(&mut st, 5.0, 1, -1);
1018 stage_m(&mut st, 9.0, 1, 1);
1019 st.integrate(&frontier(2));
1020 assert_eq!(st.errors, 1);
1021 st.publish_if_healthy();
1022 assert_eq!(st.published[&key_m()].0, 5.0);
1024
1025 st.stage_err(Timestamp::from(2), -1);
1028 st.integrate(&frontier(3));
1029 assert_eq!(st.errors, 0);
1030 st.publish_if_healthy();
1031 assert_eq!(st.published[&key_m()].0, 9.0);
1032 }
1033}