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(&self.label, 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 {
204 metric_name: usize,
205 labels: usize,
206 value: usize,
207 help: usize,
208 metric_kind: usize,
209 name_valid: usize,
210}
211
212impl ColumnIndices {
213 fn resolve(desc: &RelationDesc) -> Self {
214 let idx = |name: &str| {
215 desc.get_by_name(&ColumnName::from(name))
216 .expect("column existence validated by the SQL planner")
217 .0
218 };
219 ColumnIndices {
220 metric_name: idx("metric_name"),
221 labels: idx("labels"),
222 value: idx("value"),
223 help: idx("help"),
224 metric_kind: idx("metric_kind"),
225 name_valid: idx("name_valid"),
226 }
227 }
228}
229
230fn extract_row<'a>(
242 cols: &ColumnIndices,
243 datums: &[Datum<'a>],
244) -> (
245 &'a str,
246 Option<MetricKind>,
247 bool,
248 Vec<(&'a str, Option<&'a str>)>,
249 Option<f64>,
250 &'a str,
251) {
252 let metric_name = match datums[cols.metric_name] {
253 Datum::Null => "",
254 d => d.unwrap_str(),
255 };
256 let metric_kind = MetricKind::from_datum(datums[cols.metric_kind]);
257 let name_valid = matches!(datums[cols.name_valid], Datum::True);
258 let mut labels: Vec<(&str, Option<&str>)> = datums[cols.labels]
259 .unwrap_map()
260 .iter()
261 .map(|(k, v)| (k, (!v.is_null()).then(|| v.unwrap_str())))
262 .collect();
263 labels.sort();
264 let value = match datums[cols.value] {
265 Datum::Null => None,
266 d => Some(d.unwrap_float64()),
267 };
268 let help = datums[cols.help].unwrap_str();
269 (metric_name, metric_kind, name_valid, labels, value, help)
270}
271
272type RowKey = (
287 String,
288 Vec<(String, Option<String>)>,
289 Option<u64>,
290 Option<MetricKind>,
291 bool,
292 String,
293);
294
295type PublishedKey = (String, Vec<(String, String)>);
297type PublishedValue = (f64, MetricKind, String);
299
300#[derive(Default)]
311struct SinkState {
312 pending_ok: BTreeMap<Timestamp, BTreeMap<RowKey, i64>>,
314 pending_err: BTreeMap<Timestamp, i64>,
316 working: BTreeMap<RowKey, i64>,
318 published: BTreeMap<PublishedKey, PublishedValue>,
319 errors: i64,
322 frontier_ms: u64,
323 skipped: u64,
324 conflicts: u64,
325 collisions: u64,
326 null_values: u64,
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
333enum MetricKind {
334 Gauge,
335 Counter,
336}
337
338impl MetricKind {
339 fn from_datum(d: Datum) -> Option<Self> {
342 match d {
343 Datum::Int32(0) => Some(MetricKind::Gauge),
344 Datum::Int32(1) => Some(MetricKind::Counter),
345 _ => None,
346 }
347 }
348
349 fn proto_type(self) -> MetricType {
350 match self {
351 MetricKind::Gauge => MetricType::GAUGE,
352 MetricKind::Counter => MetricType::COUNTER,
353 }
354 }
355}
356
357fn is_valid_label_name(name: &str) -> bool {
363 let mut chars = name.chars();
364 match chars.next() {
365 Some(c) if c.is_ascii_alphabetic() || c == '_' => {
366 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
367 }
368 _ => false,
369 }
370}
371
372fn is_publishable_label_value(value: Option<&str>) -> bool {
376 matches!(value, Some(v) if !v.is_empty())
377}
378
379impl SinkState {
380 fn stage_ok(
387 &mut self,
388 metric_name: &str,
389 metric_kind: Option<MetricKind>,
390 name_valid: bool,
391 labels: &[(&str, Option<&str>)],
392 value: Option<f64>,
393 help: &str,
394 time: Timestamp,
395 diff: i64,
396 ) {
397 let key = (
398 metric_name.to_string(),
399 labels
400 .iter()
401 .map(|&(k, v)| (k.to_string(), v.map(str::to_string)))
402 .collect(),
403 value.map(f64::to_bits),
404 metric_kind,
405 name_valid,
406 help.to_string(),
407 );
408 *self
409 .pending_ok
410 .entry(time)
411 .or_default()
412 .entry(key)
413 .or_default() += diff;
414 }
415
416 fn stage_err(&mut self, time: Timestamp, diff: i64) {
418 *self.pending_err.entry(time).or_default() += diff;
419 }
420
421 fn integrate(&mut self, frontier: &Antichain<Timestamp>) {
428 let closed_ok: Vec<Timestamp> = self
429 .pending_ok
430 .keys()
431 .filter(|t| !frontier.less_equal(t))
432 .copied()
433 .collect();
434 for time in closed_ok {
435 let rows = self.pending_ok.remove(&time).expect("key from keys()");
436 for (key, diff) in rows {
437 *self.working.entry(key).or_default() += diff;
438 }
439 }
440
441 let closed_err: Vec<Timestamp> = self
442 .pending_err
443 .keys()
444 .filter(|t| !frontier.less_equal(t))
445 .copied()
446 .collect();
447 for time in closed_err {
448 self.errors += self.pending_err.remove(&time).expect("key from keys()");
449 }
450
451 self.working.retain(|_, acc| *acc != 0);
452 self.skipped = count_skipped(&self.working);
453 }
454
455 fn publish_if_healthy(&mut self) {
465 if self.errors == 0 {
466 let (published, collisions, null_values) = rebuild_published(&self.working);
467 self.published = published;
468 self.collisions = collisions;
469 self.null_values = null_values;
470 self.conflicts = count_conflicts(&self.published);
471 }
472 }
473}
474
475fn count_skipped(working: &BTreeMap<RowKey, i64>) -> u64 {
478 let mut skipped = 0u64;
479 for ((_name, labels, _bits, metric_kind, name_valid, _help), acc) in working {
480 if *acc <= 0 {
481 continue;
482 }
483 let unsupported = metric_kind.is_none();
484 let invalid = !name_valid
485 || !labels
486 .iter()
487 .all(|(k, v)| is_publishable_label_value(v.as_deref()) && is_valid_label_name(k));
488 if unsupported || invalid {
489 skipped += 1;
490 }
491 }
492 skipped
493}
494
495fn rebuild_published(
511 working: &BTreeMap<RowKey, i64>,
512) -> (BTreeMap<PublishedKey, PublishedValue>, u64, u64) {
513 let mut grouped: BTreeMap<PublishedKey, Vec<(Option<f64>, MetricKind, String)>> =
515 BTreeMap::new();
516 for ((name, labels, bits, metric_kind, name_valid, help), acc) in working {
517 if *acc <= 0 {
518 continue;
519 }
520 let Some(kind) = metric_kind else {
521 continue;
522 };
523 let Some(labels) = labels
527 .iter()
528 .map(|(k, v)| {
529 is_publishable_label_value(v.as_deref()).then(|| {
530 (
531 k.clone(),
532 v.as_ref().expect("publishable is non-null").clone(),
533 )
534 })
535 })
536 .collect::<Option<Vec<_>>>()
537 else {
538 continue;
539 };
540 if !name_valid || !labels.iter().all(|(k, _)| is_valid_label_name(k)) {
541 continue;
542 }
543 grouped.entry((name.clone(), labels)).or_default().push((
544 bits.map(f64::from_bits),
545 *kind,
546 help.clone(),
547 ));
548 }
549
550 let mut published = BTreeMap::new();
551 let mut collisions = 0u64;
552 let mut null_values = 0u64;
553 for (key, candidates) in grouped {
554 let mut non_null: Vec<PublishedValue> = candidates
555 .into_iter()
556 .filter_map(|(value, kind, help)| value.map(|v| (v, kind, help)))
557 .collect();
558 if non_null.is_empty() {
559 null_values += 1;
560 continue;
561 }
562 let mut distinct: Vec<u64> = non_null.iter().map(|(v, _, _)| v.to_bits()).collect();
563 distinct.sort_unstable();
564 distinct.dedup();
565 if distinct.len() > 1 {
566 collisions += 1;
567 }
568 non_null.sort_by(|a, b| {
569 a.0.total_cmp(&b.0)
570 .then(a.1.cmp(&b.1))
571 .then_with(|| a.2.cmp(&b.2))
572 });
573 let winner = non_null
574 .into_iter()
575 .next()
576 .expect("checked non-empty above");
577 published.insert(key, winner);
578 }
579 (published, collisions, null_values)
580}
581
582fn count_conflicts(published: &BTreeMap<PublishedKey, PublishedValue>) -> u64 {
585 let mut conflicts = 0u64;
586 let mut winner: Option<(&str, MetricKind, &str)> = None;
587 for ((name, _labels), (_value, kind, help)) in published {
588 winner = match winner {
589 Some((n, k, h)) if n == name.as_str() => {
590 if k != *kind || h != help.as_str() {
591 conflicts += 1;
592 }
593 Some((n, k, h))
594 }
595 _ => Some((name.as_str(), *kind, help.as_str())),
596 };
597 }
598 conflicts
599}
600
601fn build_families(published: &BTreeMap<PublishedKey, PublishedValue>) -> Vec<MetricFamily> {
610 let mut families = Vec::new();
611 let mut group_name: Option<&str> = None;
612 let mut family: Option<MetricFamily> = None;
613 let mut family_kind = MetricKind::Gauge;
614
615 for ((name, labels), (value, kind, help)) in published {
616 if group_name != Some(name.as_str()) {
617 if let Some(f) = family.take() {
618 families.push(f);
619 }
620 let mut mf = MetricFamily::new();
621 mf.name = Some(name.clone());
622 mf.help = Some(help.clone());
623 mf.type_ = Some(kind.proto_type().into());
624 family = Some(mf);
625 family_kind = *kind;
626 group_name = Some(name.as_str());
627 }
628
629 let mut metric = ProtoMetric::new();
630 metric.label = labels
631 .iter()
632 .map(|(k, v)| {
633 let mut lp = LabelPair::new();
634 lp.name = Some(k.clone());
635 lp.value = Some(v.clone());
636 lp
637 })
638 .collect();
639 match family_kind {
640 MetricKind::Gauge => {
641 let mut g = ProtoGauge::new();
642 g.value = Some(*value);
643 metric.gauge = Some(g).into();
644 }
645 MetricKind::Counter => {
646 let mut c = ProtoCounter::new();
647 c.value = Some(*value);
648 metric.counter = Some(c).into();
649 }
650 }
651 family
652 .as_mut()
653 .expect("initialized above for the first entry of every group")
654 .metric
655 .push(metric);
656 }
657 if let Some(f) = family.take() {
658 families.push(f);
659 }
660 families
661}
662
663#[derive(Clone)]
672struct SinkCollector {
673 state: Arc<Mutex<SinkState>>,
674 frontier_gauge: Gauge,
675 errors_gauge: Gauge,
676 skipped_gauge: Gauge,
677 conflicts_gauge: Gauge,
678 collisions_gauge: Gauge,
679 null_values_gauge: Gauge,
680}
681
682impl SinkCollector {
683 fn new(label: &str, state: Arc<Mutex<SinkState>>) -> Self {
684 let gauge = |name: &str, help: &str| {
685 Gauge::with_opts(Opts::new(name, help).const_label("sink", label))
686 .expect("static metric sink companion gauge options are valid")
687 };
688 SinkCollector {
689 state,
690 frontier_gauge: gauge(
691 "mz_compute_metric_sink_frontier_ms",
692 "The metric sink's input frontier, in milliseconds since the epoch.",
693 ),
694 errors_gauge: gauge(
695 "mz_compute_metric_sink_errors",
696 "The number of live errors on the metric sink's input.",
697 ),
698 skipped_gauge: gauge(
699 "mz_compute_metric_sink_skipped",
700 "The number of input rows skipped for an unsupported metric type, an invalid name, or a null or empty label value.",
701 ),
702 conflicts_gauge: gauge(
703 "mz_compute_metric_sink_conflicts",
704 "The number of published series whose type or help disagree with their family's chosen type or help.",
705 ),
706 collisions_gauge: gauge(
707 "mz_compute_metric_sink_collisions",
708 "The number of series with more than one distinct live value for the same metric name and labels.",
709 ),
710 null_values_gauge: gauge(
711 "mz_compute_metric_sink_null_values",
712 "The number of series currently suppressed because their value is null.",
713 ),
714 }
715 }
716}
717
718impl Collector for SinkCollector {
719 fn desc(&self) -> Vec<&Desc> {
720 let mut descs = Vec::with_capacity(6);
721 descs.extend(self.frontier_gauge.desc());
722 descs.extend(self.errors_gauge.desc());
723 descs.extend(self.skipped_gauge.desc());
724 descs.extend(self.conflicts_gauge.desc());
725 descs.extend(self.collisions_gauge.desc());
726 descs.extend(self.null_values_gauge.desc());
727 descs
728 }
729
730 fn collect(&self) -> Vec<MetricFamily> {
731 let mut families = {
732 let state = self.state.lock().expect("sink state mutex poisoned");
733 self.frontier_gauge.set(f64::cast_lossy(state.frontier_ms));
734 self.errors_gauge.set(f64::cast_lossy(state.errors));
735 self.skipped_gauge.set(f64::cast_lossy(state.skipped));
736 self.conflicts_gauge.set(f64::cast_lossy(state.conflicts));
737 self.collisions_gauge.set(f64::cast_lossy(state.collisions));
738 self.null_values_gauge
739 .set(f64::cast_lossy(state.null_values));
740 build_families(&state.published)
741 };
742
743 families.extend(self.frontier_gauge.collect());
744 families.extend(self.errors_gauge.collect());
745 families.extend(self.skipped_gauge.collect());
746 families.extend(self.conflicts_gauge.collect());
747 families.extend(self.collisions_gauge.collect());
748 families.extend(self.null_values_gauge.collect());
749 families
750 }
751}
752
753#[cfg(test)]
754mod tests {
755 use super::*;
756
757 fn frontier(bound: u64) -> Antichain<Timestamp> {
759 Antichain::from_elem(Timestamp::from(bound))
760 }
761
762 fn label_a() -> Vec<(String, String)> {
763 vec![("a".into(), "1".into())]
764 }
765
766 const LABEL_A: &[(&str, Option<&str>)] = &[("a", Some("1"))];
768
769 fn key_m() -> PublishedKey {
770 ("m".into(), label_a())
771 }
772
773 fn shaped_desc() -> RelationDesc {
777 use mz_repr::SqlScalarType;
778
779 RelationDesc::builder()
780 .with_column("metric_name", SqlScalarType::String.nullable(true))
781 .with_column(
782 "labels",
783 SqlScalarType::Map {
784 value_type: Box::new(SqlScalarType::String),
785 custom_id: None,
786 }
787 .nullable(false),
788 )
789 .with_column("value", SqlScalarType::Float64.nullable(true))
790 .with_column("help", SqlScalarType::String.nullable(false))
791 .with_column("metric_kind", SqlScalarType::Int32.nullable(true))
792 .with_column("name_valid", SqlScalarType::Bool.nullable(true))
793 .finish()
794 }
795
796 fn stage_m(st: &mut SinkState, value: f64, time: u64, diff: i64) {
798 st.stage_ok(
799 "m",
800 Some(MetricKind::Gauge),
801 true,
802 LABEL_A,
803 Some(value),
804 "h",
805 Timestamp::from(time),
806 diff,
807 );
808 }
809
810 fn stage_m_null(st: &mut SinkState, time: u64, diff: i64) {
812 st.stage_ok(
813 "m",
814 Some(MetricKind::Gauge),
815 true,
816 LABEL_A,
817 None,
818 "h",
819 Timestamp::from(time),
820 diff,
821 );
822 }
823
824 #[mz_ore::test]
825 fn fold_and_publish() {
826 let mut st = SinkState::default();
827 stage_m(&mut st, 2.0, 0, 1);
828 st.integrate(&frontier(1));
829 st.publish_if_healthy();
830 assert_eq!(st.published.len(), 1);
831 assert_eq!(st.published[&key_m()].0, 2.0);
832
833 st.stage_ok("h1", None, true, &[], Some(1.0), "h", Timestamp::from(1), 1);
835 st.integrate(&frontier(2));
836 assert_eq!(st.skipped, 1);
837
838 st.errors = 1;
841 stage_m(&mut st, 2.0, 2, -1);
842 stage_m(&mut st, 9.0, 2, 1);
843 st.integrate(&frontier(3));
844 st.publish_if_healthy();
845 assert_eq!(st.published[&key_m()].0, 2.0);
846
847 st.errors = 0;
849 st.publish_if_healthy();
850 assert_eq!(st.published[&key_m()].0, 9.0);
851 assert_eq!(st.collisions, 0);
852 }
853
854 #[mz_ore::test]
855 fn value_update_split_across_activations_no_collision() {
856 let mut st = SinkState::default();
857 stage_m(&mut st, 5.0, 0, 1);
859 st.integrate(&frontier(1));
860 st.publish_if_healthy();
861 assert_eq!(st.published[&key_m()].0, 5.0);
862
863 stage_m(&mut st, 9.0, 1, 1);
866 st.integrate(&frontier(1));
867 st.publish_if_healthy();
868 assert_eq!(st.collisions, 0);
869 stage_m(&mut st, 5.0, 1, -1);
870
871 st.integrate(&frontier(2));
873 st.publish_if_healthy();
874 assert_eq!(st.published[&key_m()].0, 9.0);
875 assert_eq!(st.collisions, 0);
876 }
877
878 #[mz_ore::test]
879 fn duplicate_multiplicity_consolidates() {
880 let mut st = SinkState::default();
881 stage_m(&mut st, 5.0, 0, 1);
883 stage_m(&mut st, 5.0, 0, 1);
884 st.integrate(&frontier(1));
885 st.publish_if_healthy();
886 assert_eq!(st.published[&key_m()].0, 5.0);
887 assert_eq!(st.collisions, 0);
888
889 stage_m(&mut st, 7.0, 1, 1);
891 st.integrate(&frontier(2));
892 st.publish_if_healthy();
893 assert_eq!(st.collisions, 1);
894 assert_eq!(st.published[&key_m()].0, 5.0);
896 }
897
898 #[mz_ore::test]
899 fn no_publish_before_time_closed() {
900 let mut st = SinkState::default();
901 stage_m(&mut st, 2.0, 5, 1);
903 st.integrate(&frontier(5));
904 st.publish_if_healthy();
905 assert!(st.published.is_empty());
906
907 st.integrate(&frontier(6));
909 st.publish_if_healthy();
910 assert_eq!(st.published[&key_m()].0, 2.0);
911 }
912
913 #[mz_ore::test]
914 fn null_value_gaps_series() {
915 let mut st = SinkState::default();
916 stage_m_null(&mut st, 1, 1);
918 st.integrate(&frontier(2));
919 st.publish_if_healthy();
920 assert!(!st.published.contains_key(&key_m()));
921 assert_eq!(st.null_values, 1);
922
923 stage_m(&mut st, 5.0, 3, 1);
926 st.integrate(&frontier(4));
927 st.publish_if_healthy();
928 assert_eq!(st.published[&key_m()].0, 5.0);
929 assert_eq!(st.null_values, 0);
930 }
931
932 #[mz_ore::test]
933 fn null_labels_become_empty() {
934 let mut st = SinkState::default();
935 st.stage_ok(
938 "m",
939 Some(MetricKind::Gauge),
940 true,
941 &[],
942 Some(1.0),
943 "h",
944 Timestamp::from(1),
945 1,
946 );
947 st.integrate(&frontier(2));
948 st.publish_if_healthy();
949 assert_eq!(st.published[&("m".into(), vec![])].0, 1.0);
950 }
951
952 #[mz_ore::test]
953 fn extract_row_normalizes_null_datums() {
954 let desc = shaped_desc();
955 let cols = ColumnIndices::resolve(&desc);
956
957 let mut row = Row::default();
958 {
959 let mut packer = row.packer();
960 packer.push(Datum::Null); packer.push_dict_with(|_| {}); packer.push(Datum::Null); packer.push(Datum::String("")); packer.push(Datum::Null); packer.push(Datum::Null); }
967
968 let datums: Vec<Datum> = row.iter().collect();
969 let (name, metric_kind, name_valid, labels, value, help) = extract_row(&cols, &datums);
970 assert_eq!(name, "");
971 assert_eq!(metric_kind, None);
972 assert!(!name_valid);
973 assert_eq!(labels, Vec::new());
974 assert_eq!(value, None);
975 assert_eq!(help, "");
976 }
977
978 #[mz_ore::test]
981 fn extract_row_keeps_null_label_values() {
982 let desc = shaped_desc();
983 let cols = ColumnIndices::resolve(&desc);
984
985 let mut row = Row::default();
986 {
987 let mut packer = row.packer();
988 packer.push(Datum::String("m"));
989 packer.push_dict_with(|row| {
990 row.push(Datum::String("bad"));
991 row.push(Datum::Null);
992 row.push(Datum::String("good"));
993 row.push(Datum::String("1"));
994 });
995 packer.push(Datum::Float64(1.0.into()));
996 packer.push(Datum::String("h"));
997 packer.push(Datum::Int32(0));
998 packer.push(Datum::True);
999 }
1000
1001 let datums: Vec<Datum> = row.iter().collect();
1002 let (_name, _metric_kind, _name_valid, labels, _value, _help) = extract_row(&cols, &datums);
1003 assert_eq!(labels, vec![("bad", None), ("good", Some("1"))]);
1004 }
1005
1006 #[mz_ore::test]
1007 fn null_or_empty_label_value_skips_row() {
1008 let mut st = SinkState::default();
1009 st.stage_ok(
1011 "m",
1012 Some(MetricKind::Gauge),
1013 true,
1014 &[("a", None)],
1015 Some(1.0),
1016 "h",
1017 Timestamp::from(1),
1018 1,
1019 );
1020 st.integrate(&frontier(2));
1021 st.publish_if_healthy();
1022 assert!(st.published.is_empty());
1023 assert_eq!(st.skipped, 1);
1024
1025 st.stage_ok(
1029 "m",
1030 Some(MetricKind::Gauge),
1031 true,
1032 &[("a", Some(""))],
1033 Some(1.0),
1034 "h",
1035 Timestamp::from(3),
1036 1,
1037 );
1038 st.integrate(&frontier(4));
1039 st.publish_if_healthy();
1040 assert!(st.published.is_empty());
1041 assert_eq!(st.skipped, 2);
1042 }
1043
1044 fn pkey(name: &str, labels: &[(&str, &str)]) -> PublishedKey {
1045 (
1046 name.to_string(),
1047 labels
1048 .iter()
1049 .map(|&(k, v)| (k.to_string(), v.to_string()))
1050 .collect(),
1051 )
1052 }
1053
1054 #[mz_ore::test]
1055 fn build_families_groups_by_name_and_kind() {
1056 let published: BTreeMap<PublishedKey, PublishedValue> = BTreeMap::from([
1057 (
1058 pkey("http_requests", &[("code", "200")]),
1059 (5.0, MetricKind::Counter, "requests".to_string()),
1060 ),
1061 (
1062 pkey("http_requests", &[("code", "500")]),
1063 (2.0, MetricKind::Counter, "requests".to_string()),
1064 ),
1065 (
1066 pkey("temp_celsius", &[]),
1067 (21.5, MetricKind::Gauge, "temperature".to_string()),
1068 ),
1069 ]);
1070
1071 let families = build_families(&published);
1072
1073 assert_eq!(families.len(), 2);
1075
1076 let requests = &families[0];
1077 assert_eq!(requests.name(), "http_requests");
1078 assert_eq!(requests.help(), "requests");
1079 let metrics = requests.get_metric();
1080 assert_eq!(metrics.len(), 2);
1081 assert_eq!(metrics[0].get_label()[0].value(), "200");
1083 assert_eq!(metrics[0].get_counter().value(), 5.0);
1084 assert_eq!(metrics[1].get_label()[0].value(), "500");
1085 assert_eq!(metrics[1].get_counter().value(), 2.0);
1086
1087 let temp = &families[1];
1088 assert_eq!(temp.name(), "temp_celsius");
1089 let temp_metrics = temp.get_metric();
1090 assert_eq!(temp_metrics.len(), 1);
1091 assert_eq!(temp_metrics[0].get_gauge().value(), 21.5);
1092 }
1093
1094 #[mz_ore::test]
1095 fn count_conflicts_flags_type_and_help_disagreement() {
1096 let published: BTreeMap<PublishedKey, PublishedValue> = BTreeMap::from([
1099 (
1100 pkey("m", &[("a", "1")]),
1101 (1.0, MetricKind::Gauge, "h1".to_string()),
1102 ),
1103 (
1104 pkey("m", &[("b", "2")]),
1105 (2.0, MetricKind::Counter, "h1".to_string()),
1106 ),
1107 (
1108 pkey("m", &[("c", "3")]),
1109 (3.0, MetricKind::Gauge, "h2".to_string()),
1110 ),
1111 (
1112 pkey("other", &[]),
1113 (1.0, MetricKind::Gauge, "h".to_string()),
1114 ),
1115 ]);
1116 assert_eq!(count_conflicts(&published), 2);
1117
1118 let consistent: BTreeMap<PublishedKey, PublishedValue> = BTreeMap::from([
1120 (
1121 pkey("m", &[("a", "1")]),
1122 (1.0, MetricKind::Gauge, "h".to_string()),
1123 ),
1124 (
1125 pkey("m", &[("b", "2")]),
1126 (2.0, MetricKind::Gauge, "h".to_string()),
1127 ),
1128 ]);
1129 assert_eq!(count_conflicts(&consistent), 0);
1130 }
1131
1132 #[mz_ore::test]
1133 fn err_stream_freezes_and_recovers() {
1134 let mut st = SinkState::default();
1135 stage_m(&mut st, 5.0, 0, 1);
1137 st.integrate(&frontier(1));
1138 st.publish_if_healthy();
1139 assert_eq!(st.published[&key_m()].0, 5.0);
1140
1141 st.stage_err(Timestamp::from(1), 1);
1144 stage_m(&mut st, 5.0, 1, -1);
1145 stage_m(&mut st, 9.0, 1, 1);
1146 st.integrate(&frontier(2));
1147 assert_eq!(st.errors, 1);
1148 st.publish_if_healthy();
1149 assert_eq!(st.published[&key_m()].0, 5.0);
1151
1152 st.stage_err(Timestamp::from(2), -1);
1155 st.integrate(&frontier(3));
1156 assert_eq!(st.errors, 0);
1157 st.publish_if_healthy();
1158 assert_eq!(st.published[&key_m()].0, 9.0);
1159 }
1160
1161 #[mz_ore::test]
1165 fn collector_labels_gauges_with_sink_label() {
1166 let state = Arc::new(Mutex::new(SinkState::default()));
1167 let collector = SinkCollector::new("mz_curated_example", state);
1168
1169 let families = collector.collect();
1170 assert!(!families.is_empty());
1174 for family in &families {
1175 for metric in family.get_metric() {
1176 let sink = metric
1177 .get_label()
1178 .iter()
1179 .find(|l| l.name() == "sink")
1180 .expect("sink label present");
1181 assert_eq!(sink.value(), "mz_curated_example");
1182 }
1183 }
1184 }
1185}