1use std::sync::{Arc, Mutex};
11
12use mz_compute_client::metrics::{CommandMetrics, HistoryMetrics};
13use mz_ore::cast::CastFrom;
14use mz_ore::metric;
15use mz_ore::metrics::{
16 MakeCollectorOpts, MetricTag, MetricVisibility, MetricsRegistry, UIntGauge, raw,
17};
18use mz_repr::{GlobalId, SharedRow};
19use prometheus::core::{AtomicF64, GenericCounter};
20use prometheus::proto::LabelPair;
21use prometheus::{Histogram, HistogramVec, IntCounter};
22
23#[derive(Clone, Debug)]
33pub struct ComputeMetrics {
34 workload_class: Arc<Mutex<Option<String>>>,
36
37 history_command_count: raw::UIntGaugeVec,
39 history_dataflow_count: raw::UIntGaugeVec,
40
41 reconciliation_reused_dataflows_count_total: raw::IntCounterVec,
43 reconciliation_replaced_dataflows_count_total: raw::IntCounterVec,
44
45 arrangement_maintenance_seconds_total: raw::CounterVec,
47 arrangement_maintenance_active_info: raw::UIntGaugeVec,
48
49 timely_step_duration_seconds: HistogramVec,
60 persist_peek_seconds: HistogramVec,
61 handle_command_duration_seconds: HistogramVec,
62
63 index_peek_total_seconds: Histogram,
65 index_peek_seek_fulfillment_seconds: Histogram,
66 index_peek_error_scan_seconds: Histogram,
67 index_peek_cursor_setup_seconds: Histogram,
68 index_peek_row_iteration_seconds: Histogram,
69 index_peek_row_iteration_rows: Histogram,
70 index_peek_result_sort_seconds: Histogram,
71 index_peek_result_sort_rows: Histogram,
72 index_peek_frontier_check_seconds: Histogram,
73 index_peek_row_collection_seconds: Histogram,
74 index_peek_walks_total: raw::IntCounterVec,
75 index_peek_stashed_total: IntCounter,
76 index_peek_permit_queue_depth: UIntGauge,
77 index_peek_permit_wait_seconds: Histogram,
78 index_peek_offload_seconds: Histogram,
79
80 shared_row_heap_capacity_bytes: raw::UIntGaugeVec,
82
83 replica_expiration_timestamp_seconds: raw::UIntGaugeVec,
85 replica_expiration_remaining_seconds: raw::GaugeVec,
86
87 collection_count: raw::UIntGaugeVec,
89
90 subscribe_snapshots_skipped_total: IntCounter,
92}
93
94fn with_role(
101 mut opts: MakeCollectorOpts,
102 role: crate::server::ComputeRuntimeRole,
103) -> MakeCollectorOpts {
104 if let Some(label) = role.label() {
105 opts.opts = opts.opts.const_label("role", label);
106 }
107 opts
108}
109
110impl ComputeMetrics {
111 pub fn register_with(
117 registry: &MetricsRegistry,
118 role: crate::server::ComputeRuntimeRole,
119 ) -> Self {
120 let workload_class = Arc::new(Mutex::new(None));
121 let mut index_peek_row_buckets =
122 prometheus::exponential_buckets(1.0, 2.0, 25).expect("valid parameters");
123 index_peek_row_buckets.insert(0, 0.0);
124
125 if role.owns_process_globals() {
132 registry.register_postprocessor({
133 let workload_class = Arc::clone(&workload_class);
134 move |metrics| {
135 let workload_class: Option<String> =
136 workload_class.lock().expect("lock poisoned").clone();
137 let Some(workload_class) = workload_class else {
138 return;
139 };
140 for metric in metrics {
141 for metric in metric.mut_metric() {
142 let mut label = LabelPair::default();
143 label.set_name("workload_class".into());
144 label.set_value(workload_class.clone());
145
146 let mut labels = metric.take_label();
147 labels.push(label);
148 metric.set_label(labels);
149 }
150 }
151 }
152 });
153 }
154
155 Self {
156 workload_class,
157 history_command_count: registry.register(with_role(metric!(
158 name: "mz_compute_replica_history_command_count",
159 help: "The number of commands in the replica's command history.",
160 var_labels: ["worker_id", "command_type"],
161 ), role)),
162 history_dataflow_count: registry.register(with_role(metric!(
163 name: "mz_compute_replica_history_dataflow_count",
164 help: "The number of dataflows in the replica's command history.",
165 var_labels: ["worker_id"],
166 visibility: MetricVisibility::Public,
167 tags: [MetricTag::Compute],
168 ), role)),
169 reconciliation_reused_dataflows_count_total: registry.register(with_role(metric!(
170 name: "mz_compute_reconciliation_reused_dataflows_count_total",
171 help: "The total number of dataflows that were reused during compute reconciliation.",
172 var_labels: ["worker_id"],
173 ), role)),
174 reconciliation_replaced_dataflows_count_total: registry.register(with_role(metric!(
175 name: "mz_compute_reconciliation_replaced_dataflows_count_total",
176 help: "The total number of dataflows that were replaced during compute reconciliation.",
177 var_labels: ["worker_id", "reason"],
178 ), role)),
179 arrangement_maintenance_seconds_total: registry.register(with_role(metric!(
180 name: "mz_arrangement_maintenance_seconds_total",
181 help: "The total time spent maintaining arrangements.",
182 var_labels: ["worker_id"],
183 visibility: MetricVisibility::Public,
184 tags: [MetricTag::Compute],
185 ), role)),
186 arrangement_maintenance_active_info: registry.register(with_role(metric!(
187 name: "mz_arrangement_maintenance_active_info",
188 help: "Whether maintenance is currently occuring.",
189 var_labels: ["worker_id"],
190 ), role)),
191 timely_step_duration_seconds: registry.register(with_role(metric!(
192 name: "mz_timely_step_duration_seconds",
193 help: "The time spent in each compute step_or_park call",
194 const_labels: {"cluster" => "compute"},
195 var_labels: ["worker_id"],
196 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 32.0),
197 ), role)),
198 shared_row_heap_capacity_bytes: registry.register(with_role(metric!(
199 name: "mz_dataflow_shared_row_heap_capacity_bytes",
200 help: "The heap capacity of the shared row.",
201 var_labels: ["worker_id"],
202 ), role)),
203 persist_peek_seconds: registry.register(with_role(metric!(
204 name: "mz_persist_peek_seconds",
205 help: "Time spent in (experimental) Persist fast-path peeks.",
206 var_labels: ["worker_id"],
207 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
208 ), role)),
209 handle_command_duration_seconds: registry.register(with_role(metric!(
210 name: "mz_cluster_handle_command_duration_seconds",
211 help: "Time spent in handling commands.",
212 const_labels: {"cluster" => "compute"},
213 var_labels: ["worker_id", "command_type"],
214 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
215 ), role)),
216 index_peek_total_seconds: registry.register(with_role(metric!(
217 name: "mz_index_peek_total_seconds",
218 help: "Time one visit to an index peek spent on the timely worker. A peek whose walk was offloaded contributes only the inline slice that offloaded it, and its time away from the worker is `mz_index_peek_offload_seconds`.",
219 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
220 ), role)),
221 index_peek_seek_fulfillment_seconds: registry.register(with_role(metric!(
222 name: "mz_index_peek_seek_fulfillment_seconds",
223 help: "Time in seek_fulfillment method including frontier checks and data collection.",
224 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
225 ), role)),
226 index_peek_error_scan_seconds: registry.register(with_role(metric!(
227 name: "mz_index_peek_error_scan_seconds",
228 help: "Time scanning the error trace for errors, summed over the slices the scan was cut into and observed only for scans that find no error.",
229 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
230 ), role)),
231 index_peek_cursor_setup_seconds: registry.register(with_role(metric!(
232 name: "mz_index_peek_cursor_setup_seconds",
233 help: "Time opening the trace cursor and sorting the literal constraints, excluding the seek to those literals.",
234 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
235 ), role)),
236 index_peek_row_iteration_seconds: registry.register(with_role(metric!(
237 name: "mz_index_peek_row_iteration_seconds",
238 help: "Time iterating rows, seeking the cursor to the literal constraints, and evaluating MFP, summed over the slices the walk was cut into.",
239 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
240 ), role)),
241 index_peek_row_iteration_rows: registry.register(with_role(metric!(
242 name: "mz_index_peek_row_iteration_rows",
243 help: "Number of arrangement rows evaluated by the index peek result iterator.",
244 buckets: index_peek_row_buckets.clone(),
245 ), role)),
246 index_peek_result_sort_seconds: registry.register(with_role(metric!(
247 name: "mz_index_peek_result_sort_seconds",
248 help: "Time thinning intermediate results down to the rows a peek's finishing needs.",
249 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
250 ), role)),
251 index_peek_result_sort_rows: registry.register(with_role(metric!(
252 name: "mz_index_peek_result_sort_rows",
253 help: "Number of intermediate result rows handed to thinning during peek collection, summed across the times it ran.",
254 buckets: index_peek_row_buckets,
255 ), role)),
256 index_peek_frontier_check_seconds: registry.register(with_role(metric!(
257 name: "mz_index_peek_frontier_check_seconds",
258 help: "Time checking trace frontiers.",
259 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
260 ), role)),
261 index_peek_row_collection_seconds: registry.register(with_role(metric!(
262 name: "mz_index_peek_row_collection_seconds",
263 help: "Time constructing RowCollection from peek results, including converting the row counts the scan produced.",
264 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
265 ), role)),
266 index_peek_walks_total: registry.register(with_role(metric!(
267 name: "mz_index_peek_walks_total",
268 help: "The number of index peek walks that reached an outcome, by the substrate they ended on: `inline` on the timely worker, `offloaded` away from it.",
269 var_labels: ["substrate"],
270 ), role)),
271 index_peek_stashed_total: registry.register(with_role(metric!(
272 name: "mz_index_peek_stashed_total",
273 help: "The number of index peek walks that answered with a handle to the peek response stash, always a subset of the `offloaded` substrate of `mz_index_peek_walks_total`.",
274 ), role)),
275 index_peek_permit_queue_depth: registry.register(with_role(metric!(
276 name: "mz_index_peek_permit_queue_depth",
277 help: "The number of offloaded index peek walks waiting for a permit to run.",
278 ), role)),
279 index_peek_permit_wait_seconds: registry.register(with_role(metric!(
280 name: "mz_index_peek_permit_wait_seconds",
281 help: "Time an offloaded index peek walk waited for a permit, observed only for walks that were admitted.",
282 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
283 ), role)),
284 index_peek_offload_seconds: registry.register(with_role(metric!(
285 name: "mz_index_peek_offload_seconds",
286 help: "Wall-clock time an offloaded index peek walk spent away from the timely worker, including the wait for a permit.",
287 buckets: mz_ore::stats::histogram_seconds_buckets(0.000_128, 8.0),
288 ), role)),
289 replica_expiration_timestamp_seconds: registry.register(with_role(metric!(
290 name: "mz_dataflow_replica_expiration_timestamp_seconds",
291 help: "The replica expiration timestamp in seconds since epoch.",
292 var_labels: ["worker_id"],
293 ), role)),
294 replica_expiration_remaining_seconds: registry.register(with_role(metric!(
295 name: "mz_dataflow_replica_expiration_remaining_seconds",
296 help: "The remaining seconds until replica expiration. Can go negative, can lag behind.",
297 var_labels: ["worker_id"],
298 ), role)),
299 collection_count: registry.register(with_role(metric!(
300 name: "mz_compute_collection_count",
301 help: "The number and hydration status of maintained compute collections.",
302 var_labels: ["worker_id", "type", "hydrated"],
303 ), role)),
304 subscribe_snapshots_skipped_total: registry.register(with_role(metric!(
305 name: "mz_subscribe_snapshots_skipped_total",
306 help: "The number of collection snapshots that were skipped by the subscribe snapshot optimization.",
307 ), role)),
308 }
309 }
310
311 pub fn set_workload_class(&self, workload_class: Option<String>) {
313 let mut guard = self.workload_class.lock().expect("lock poisoned");
314 *guard = workload_class
315 }
316
317 pub fn for_worker(&self, worker_id: usize) -> WorkerMetrics {
318 let worker = worker_id.to_string();
319 let arrangement_maintenance_seconds_total = self
320 .arrangement_maintenance_seconds_total
321 .with_label_values(&[&worker]);
322 let arrangement_maintenance_active_info = self
323 .arrangement_maintenance_active_info
324 .with_label_values(&[&worker]);
325 let timely_step_duration_seconds = self
326 .timely_step_duration_seconds
327 .with_label_values(&[&worker]);
328 let persist_peek_seconds = self.persist_peek_seconds.with_label_values(&[&worker]);
329 let handle_command_duration_seconds = CommandMetrics::build(|typ| {
330 self.handle_command_duration_seconds
331 .with_label_values(&[worker.as_ref(), typ])
332 });
333 let index_peek_total_seconds = self.index_peek_total_seconds.clone();
334 let index_peek_seek_fulfillment_seconds = self.index_peek_seek_fulfillment_seconds.clone();
335 let index_peek_error_scan_seconds = self.index_peek_error_scan_seconds.clone();
336 let index_peek_cursor_setup_seconds = self.index_peek_cursor_setup_seconds.clone();
337 let index_peek_row_iteration_seconds = self.index_peek_row_iteration_seconds.clone();
338 let index_peek_row_iteration_rows = self.index_peek_row_iteration_rows.clone();
339 let index_peek_result_sort_seconds = self.index_peek_result_sort_seconds.clone();
340 let index_peek_result_sort_rows = self.index_peek_result_sort_rows.clone();
341 let index_peek_frontier_check_seconds = self.index_peek_frontier_check_seconds.clone();
342 let index_peek_row_collection_seconds = self.index_peek_row_collection_seconds.clone();
343 let index_peek_walks_inline = self.index_peek_walks_total.with_label_values(&["inline"]);
344 let index_peek_walks_offloaded = self
345 .index_peek_walks_total
346 .with_label_values(&["offloaded"]);
347 let index_peek_stashed_total = self.index_peek_stashed_total.clone();
348 let index_peek_permit_queue_depth = self.index_peek_permit_queue_depth.clone();
349 let index_peek_permit_wait_seconds = self.index_peek_permit_wait_seconds.clone();
350 let index_peek_offload_seconds = self.index_peek_offload_seconds.clone();
351 let replica_expiration_timestamp_seconds = self
352 .replica_expiration_timestamp_seconds
353 .with_label_values(&[&worker]);
354 let replica_expiration_remaining_seconds = self
355 .replica_expiration_remaining_seconds
356 .with_label_values(&[&worker]);
357 let shared_row_heap_capacity_bytes = self
358 .shared_row_heap_capacity_bytes
359 .with_label_values(&[&worker]);
360
361 WorkerMetrics {
362 worker_label: worker,
363 metrics: self.clone(),
364 arrangement_maintenance_seconds_total,
365 arrangement_maintenance_active_info,
366 timely_step_duration_seconds,
367 persist_peek_seconds,
368 handle_command_duration_seconds,
369 index_peek_total_seconds,
370 index_peek_seek_fulfillment_seconds,
371 index_peek_error_scan_seconds,
372 index_peek_cursor_setup_seconds,
373 index_peek_row_iteration_seconds,
374 index_peek_row_iteration_rows,
375 index_peek_result_sort_seconds,
376 index_peek_result_sort_rows,
377 index_peek_frontier_check_seconds,
378 index_peek_row_collection_seconds,
379 index_peek_walks_inline,
380 index_peek_walks_offloaded,
381 index_peek_stashed_total,
382 index_peek_permit_queue_depth,
383 index_peek_permit_wait_seconds,
384 index_peek_offload_seconds,
385 replica_expiration_timestamp_seconds,
386 replica_expiration_remaining_seconds,
387 shared_row_heap_capacity_bytes,
388 }
389 }
390}
391
392#[derive(Clone, Debug)]
394pub struct WorkerMetrics {
395 worker_label: String,
396 metrics: ComputeMetrics,
397
398 pub(crate) arrangement_maintenance_seconds_total: GenericCounter<AtomicF64>,
400 pub(crate) arrangement_maintenance_active_info: UIntGauge,
406 pub(crate) timely_step_duration_seconds: Histogram,
408 pub(crate) persist_peek_seconds: Histogram,
410 pub(crate) handle_command_duration_seconds: CommandMetrics<Histogram>,
412 pub(crate) index_peek_total_seconds: Histogram,
414 pub(crate) index_peek_seek_fulfillment_seconds: Histogram,
416 pub(crate) index_peek_error_scan_seconds: Histogram,
418 pub(crate) index_peek_cursor_setup_seconds: Histogram,
420 pub(crate) index_peek_row_iteration_seconds: Histogram,
422 pub(crate) index_peek_row_iteration_rows: Histogram,
424 pub(crate) index_peek_result_sort_seconds: Histogram,
426 pub(crate) index_peek_result_sort_rows: Histogram,
428 pub(crate) index_peek_frontier_check_seconds: Histogram,
430 pub(crate) index_peek_row_collection_seconds: Histogram,
432 pub(crate) index_peek_walks_inline: IntCounter,
438 pub(crate) index_peek_walks_offloaded: IntCounter,
440 pub(crate) index_peek_stashed_total: IntCounter,
445 pub(crate) index_peek_permit_queue_depth: UIntGauge,
447 pub(crate) index_peek_permit_wait_seconds: Histogram,
449 pub(crate) index_peek_offload_seconds: Histogram,
451 pub(crate) replica_expiration_timestamp_seconds: UIntGauge,
453 pub(crate) replica_expiration_remaining_seconds: raw::Gauge,
455 shared_row_heap_capacity_bytes: UIntGauge,
457}
458
459impl WorkerMetrics {
460 pub fn for_history(&self) -> HistoryMetrics<UIntGauge> {
461 let command_counts = CommandMetrics::build(|typ| {
462 self.metrics
463 .history_command_count
464 .with_label_values(&[self.worker_label.as_ref(), typ])
465 });
466 let dataflow_count = self
467 .metrics
468 .history_dataflow_count
469 .with_label_values(&[&self.worker_label]);
470
471 HistoryMetrics {
472 command_counts,
473 dataflow_count,
474 }
475 }
476
477 pub fn record_dataflow_reconciliation(
491 &self,
492 compatible: bool,
493 uncompacted: bool,
494 subscribe_free: bool,
495 copy_to_free: bool,
496 dependencies_retained: bool,
497 ) {
498 if !compatible {
499 self.metrics
500 .reconciliation_replaced_dataflows_count_total
501 .with_label_values(&[self.worker_label.as_ref(), "incompatible"])
502 .inc();
503 } else if !uncompacted {
504 self.metrics
505 .reconciliation_replaced_dataflows_count_total
506 .with_label_values(&[self.worker_label.as_ref(), "compacted"])
507 .inc();
508 } else if !subscribe_free {
509 self.metrics
510 .reconciliation_replaced_dataflows_count_total
511 .with_label_values(&[self.worker_label.as_ref(), "subscribe"])
512 .inc();
513 } else if !copy_to_free {
514 self.metrics
515 .reconciliation_replaced_dataflows_count_total
516 .with_label_values(&[self.worker_label.as_ref(), "copy-to"])
517 .inc();
518 } else if !dependencies_retained {
519 self.metrics
520 .reconciliation_replaced_dataflows_count_total
521 .with_label_values(&[self.worker_label.as_ref(), "dependency"])
522 .inc();
523 } else {
524 self.metrics
525 .reconciliation_reused_dataflows_count_total
526 .with_label_values(&[&self.worker_label])
527 .inc();
528 }
529 }
530
531 pub fn record_shared_row_metrics(&self) {
533 let binding = SharedRow::get();
534 self.shared_row_heap_capacity_bytes
535 .set(u64::cast_from(binding.byte_capacity()));
536 }
537
538 fn inc_collection_count(&self, collection_type: &str, hydrated: bool) {
540 let hydrated = if hydrated { "1" } else { "0" };
541 self.metrics
542 .collection_count
543 .with_label_values(&[self.worker_label.as_ref(), collection_type, hydrated])
544 .inc();
545 }
546
547 fn dec_collection_count(&self, collection_type: &str, hydrated: bool) {
549 let hydrated = if hydrated { "1" } else { "0" };
550 self.metrics
551 .collection_count
552 .with_label_values(&[self.worker_label.as_ref(), collection_type, hydrated])
553 .dec();
554 }
555
556 pub fn inc_subscribe_snapshot_optimization(&self) {
557 self.metrics.subscribe_snapshots_skipped_total.inc()
558 }
559
560 pub fn set_workload_class(&self, workload_class: Option<String>) {
562 self.metrics.set_workload_class(workload_class);
563 }
564
565 pub fn for_collection(&self, id: GlobalId) -> CollectionMetrics {
566 CollectionMetrics::new(id, self.clone())
567 }
568}
569
570#[derive(Clone, Debug)]
576pub struct CollectionMetrics {
577 metrics: WorkerMetrics,
578 collection_type: &'static str,
579 collection_hydrated: bool,
580}
581
582impl CollectionMetrics {
583 pub fn new(collection_id: GlobalId, metrics: WorkerMetrics) -> Self {
584 let collection_type = match collection_id {
585 GlobalId::System(_) => "system",
586 GlobalId::IntrospectionSourceIndex(_) => "log",
587 GlobalId::User(_) => "user",
588 GlobalId::Transient(_) => "transient",
589 GlobalId::Explain => "explain",
590 };
591 let collection_hydrated = false;
592
593 metrics.inc_collection_count(collection_type, collection_hydrated);
594
595 Self {
596 metrics,
597 collection_type,
598 collection_hydrated,
599 }
600 }
601
602 pub fn record_collection_hydrated(&mut self) {
604 if self.collection_hydrated {
605 return;
606 }
607
608 self.metrics
609 .dec_collection_count(self.collection_type, false);
610 self.metrics
611 .inc_collection_count(self.collection_type, true);
612 self.collection_hydrated = true;
613 }
614}
615
616impl Drop for CollectionMetrics {
617 fn drop(&mut self) {
618 self.metrics
619 .dec_collection_count(self.collection_type, self.collection_hydrated);
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use std::collections::BTreeSet;
626
627 use mz_ore::metrics::MetricsRegistry;
628
629 use super::ComputeMetrics;
630 use crate::server::ComputeRuntimeRole;
631
632 #[mz_ore::test]
636 fn solo_runtime_omits_role_label() {
637 let registry = MetricsRegistry::new();
638 let metrics = ComputeMetrics::register_with(®istry, ComputeRuntimeRole::Solo);
639 let _worker = metrics.for_worker(0);
641
642 for family in registry.gather() {
643 for metric in family.get_metric() {
644 for label in metric.get_label() {
645 assert_ne!(
646 label.name(),
647 "role",
648 "solo metric {} unexpectedly carries a role label",
649 family.name(),
650 );
651 }
652 }
653 }
654 }
655
656 #[mz_ore::test]
660 fn named_roles_carry_distinct_role_label() {
661 let registry = MetricsRegistry::new();
662 let maintenance = ComputeMetrics::register_with(®istry, ComputeRuntimeRole::Maintenance);
663 let interactive = ComputeMetrics::register_with(®istry, ComputeRuntimeRole::Interactive);
664 let _maintenance_worker = maintenance.for_worker(0);
665 let _interactive_worker = interactive.for_worker(0);
666
667 let mut roles = BTreeSet::new();
668 for family in registry.gather() {
669 for metric in family.get_metric() {
670 let role = metric
671 .get_label()
672 .iter()
673 .find(|label| label.name() == "role")
674 .unwrap_or_else(|| panic!("metric {} missing a role label", family.name()));
675 roles.insert(role.value().to_string());
676 }
677 }
678
679 assert!(roles.contains("maintenance"), "roles seen: {roles:?}");
680 assert!(roles.contains("interactive"), "roles seen: {roles:?}");
681 }
682}