1use prometheus::{
4 Gauge, Histogram, HistogramOpts, HistogramVec, IntCounter, IntCounterVec, IntGauge,
5 IntGaugeVec, Opts, Registry,
6};
7
8#[cfg(feature = "cluster")]
10pub struct ClusterSubscriptionMetrics {
11 pub active_readers: IntGauge,
13 pub open_total: IntCounter,
15 pub open_failures_total: IntCounter,
17 pub frames_committed_total: IntCounter,
19 pub rows_committed_total: IntCounter,
21 pub bytes_committed_total: IntCounter,
23 pub segments_written_total: IntCounter,
25 pub segment_write_failures_total: IntCounter,
27 pub manifest_failures_total: IntCounter,
29 pub integrity_failures_total: IntCounter,
31 pub stale_writer_rejections_total: IntCounter,
33 pub sequence_gaps_total: IntCounter,
35 pub replay_bytes_total: IntCounter,
37 pub replay_frames_total: IntCounter,
39 pub replay_pruned_total: IntCounter,
41 pub gateway_lag_disconnects_total: IntCounter,
43 pub pending_bytes: IntGauge,
45 pub retained_bytes: IntGauge,
47 pub orphan_bytes: IntGauge,
49 pub checkpoint_prepare_seconds: Histogram,
51 pub commit_visibility_seconds: Histogram,
53 pub gateway_manifest_refresh_seconds: Histogram,
55}
56
57#[cfg(feature = "cluster")]
58impl ClusterSubscriptionMetrics {
59 fn new(registry: &Registry) -> Self {
60 Self {
61 active_readers: register_int_gauge(
62 registry,
63 "cluster_subscription_active_readers",
64 "Committed cluster subscription readers attached to this process",
65 ),
66 open_total: register_int_counter(
67 registry,
68 "cluster_subscription_open_total",
69 "Committed cluster subscription open attempts",
70 ),
71 open_failures_total: register_int_counter(
72 registry,
73 "cluster_subscription_open_failures_total",
74 "Failed committed cluster subscription open attempts",
75 ),
76 frames_committed_total: register_int_counter(
77 registry,
78 "cluster_subscription_frames_committed_total",
79 "Output frames made visible by authoritative checkpoint commits",
80 ),
81 rows_committed_total: register_int_counter(
82 registry,
83 "cluster_subscription_rows_committed_total",
84 "Output rows made visible by authoritative checkpoint commits",
85 ),
86 bytes_committed_total: register_int_counter(
87 registry,
88 "cluster_subscription_bytes_committed_total",
89 "Encoded output bytes made visible by authoritative checkpoint commits",
90 ),
91 segments_written_total: register_int_counter(
92 registry,
93 "cluster_subscription_segments_written_total",
94 "Immutable committed-output segments written by this process",
95 ),
96 segment_write_failures_total: register_int_counter(
97 registry,
98 "cluster_subscription_segment_write_failures_total",
99 "Immutable committed-output segment writes that failed",
100 ),
101 manifest_failures_total: register_int_counter(
102 registry,
103 "cluster_subscription_manifest_failures_total",
104 "Committed-output manifest construction, loading, or validation failures",
105 ),
106 integrity_failures_total: register_int_counter(
107 registry,
108 "cluster_subscription_integrity_failures_total",
109 "Committed-output manifest or segment integrity failures",
110 ),
111 stale_writer_rejections_total: register_int_counter(
112 registry,
113 "cluster_subscription_stale_writer_rejections_total",
114 "Fenced committed-output writer attempts rejected by this process",
115 ),
116 sequence_gaps_total: register_int_counter(
117 registry,
118 "cluster_subscription_sequence_gaps_total",
119 "Partition sequence continuity failures detected by writers or readers",
120 ),
121 replay_bytes_total: register_int_counter(
122 registry,
123 "cluster_subscription_replay_bytes_total",
124 "Encoded bytes read during committed subscription replay",
125 ),
126 replay_frames_total: register_int_counter(
127 registry,
128 "cluster_subscription_replay_frames_total",
129 "Frames read during committed subscription replay",
130 ),
131 replay_pruned_total: register_int_counter(
132 registry,
133 "cluster_subscription_replay_pruned_total",
134 "Replay opens rejected because their epoch was pruned",
135 ),
136 gateway_lag_disconnects_total: register_int_counter(
137 registry,
138 "cluster_subscription_gateway_lag_disconnects_total",
139 "Readers disconnected after exhausting bounded gateway lag allowance",
140 ),
141 pending_bytes: register_int_gauge(
142 registry,
143 "cluster_subscription_pending_bytes",
144 "In-memory committed-output bytes awaiting checkpoint disposition",
145 ),
146 retained_bytes: register_int_gauge(
147 registry,
148 "cluster_subscription_retained_bytes",
149 "Bytes reachable from the retained committed output-log suffix",
150 ),
151 orphan_bytes: register_int_gauge(
152 registry,
153 "cluster_subscription_orphan_bytes",
154 "Grace-held unreachable output bytes found by the latest orphan scan",
155 ),
156 checkpoint_prepare_seconds: register_subscription_histogram(
157 registry,
158 "cluster_subscription_checkpoint_prepare_seconds",
159 "Subscription output encoding and upload duration during checkpoint preparation",
160 ),
161 commit_visibility_seconds: register_subscription_histogram(
162 registry,
163 "cluster_subscription_commit_visibility_seconds",
164 "Authoritative checkpoint Commit publication duration before local visibility",
165 ),
166 gateway_manifest_refresh_seconds: register_subscription_histogram(
167 registry,
168 "cluster_subscription_gateway_manifest_refresh_seconds",
169 "Gateway committed-index refresh duration",
170 ),
171 }
172 }
173}
174
175fn register_int_counter(registry: &Registry, name: &str, help: &str) -> IntCounter {
176 let metric = IntCounter::new(name, help).unwrap();
177 registry.register(Box::new(metric.clone())).unwrap();
178 metric
179}
180
181fn register_event_counters(registry: &Registry) -> (IntCounter, IntCounter) {
182 (
183 register_int_counter(
184 registry,
185 "events_ingested_total",
186 "Events ingested from sources",
187 ),
188 register_int_counter(
189 registry,
190 "events_emitted_total",
191 "Events emitted to streams",
192 ),
193 )
194}
195
196#[cfg(feature = "cluster")]
197fn register_int_gauge(registry: &Registry, name: &str, help: &str) -> IntGauge {
198 let metric = IntGauge::new(name, help).unwrap();
199 registry.register(Box::new(metric.clone())).unwrap();
200 metric
201}
202
203#[cfg(feature = "cluster")]
204fn register_subscription_histogram(registry: &Registry, name: &str, help: &str) -> Histogram {
205 let metric = Histogram::with_opts(
206 HistogramOpts::new(name, help)
207 .buckets(prometheus::exponential_buckets(0.001, 2.0, 18).unwrap()),
208 )
209 .unwrap();
210 registry.register(Box::new(metric.clone())).unwrap();
211 metric
212}
213
214pub struct EngineMetrics {
219 #[cfg(feature = "cluster")]
221 pub cluster_subscription: ClusterSubscriptionMetrics,
222 pub events_ingested: IntCounter,
224 pub events_emitted: IntCounter,
226 pub events_dropped: IntCounter,
228 pub cycles: IntCounter,
230 pub batches: IntCounter,
232 pub queries_compiled: IntCounter,
234 pub queries_cached_plan: IntCounter,
236 pub cycles_backpressured: IntCounter,
238 pub mv_updates: IntCounter,
240 pub mv_bytes_stored: IntGauge,
242 pub pipeline_watermark: IntGauge,
244 pub source_watermark_ms: IntGaugeVec,
246 pub source_idle: IntGaugeVec,
249 pub stream_watermark_ms: IntGaugeVec,
251 pub input_buf_bytes: IntGaugeVec,
253 pub managed_state_accounted_bytes: IntGaugeVec,
259 pub shed_records_total: IntCounterVec,
261 pub checkpoints_completed: IntCounter,
263 pub checkpoints_failed: IntCounter,
265 pub checkpoint_epoch: IntGauge,
267 pub checkpoint_size_bytes: IntGauge,
269 pub sink_write_failures: IntCounter,
271 pub sink_write_timeouts: IntCounter,
273 pub sink_task_channel_closed: IntCounter,
275 pub sink_filter_rejected_rows: IntCounterVec,
278 pub window_late_dropped: IntCounter,
281 pub events_null_timestamp: IntCounter,
283 pub temporal_filter_buffered: IntGauge,
285 pub temporal_filter_inserts: IntCounter,
287 pub temporal_filter_retracts: IntCounter,
289 pub temporal_filter_dropped: IntCounter,
291 pub cycle_duration: Histogram,
293 pub cycle_execute_duration: Histogram,
295 pub cycle_output_store_duration: Histogram,
297 pub cycle_sink_enqueue_duration: Histogram,
299 pub operator_process_duration: HistogramVec,
302 pub checkpoint_duration: Histogram,
304 pub checkpoint_state_capture_duration: Histogram,
306 pub checkpoint_pipeline_stall_duration: Histogram,
310 pub checkpoint_barrier_local_duration: Histogram,
313 pub checkpoint_aligned_resume_wait: Histogram,
316 pub sink_precommit_duration: Histogram,
318 pub lookup_cache_hits: IntCounterVec,
320 pub lookup_cache_misses: IntCounterVec,
322 pub lookup_source_errors: IntCounterVec,
324 pub lookup_in_flight_rows: IntGaugeVec,
326 pub placement_vnodes_per_domain: IntGaugeVec,
328 pub placement_blast_radius_ratio: Gauge,
330 pub pipeline_faults_total: IntCounter,
332 pub pipeline_cycle_errors_total: IntCounter,
334 pub pipeline_restarts_total: IntCounter,
336 pub coordinated_recoveries_total: IntCounter,
338 pub coordinated_recovery_failures_total: IntCounter,
340 pub shuffle_delivery_loss_incidents_total: IntCounter,
342}
343
344impl EngineMetrics {
345 #[must_use]
351 #[allow(clippy::too_many_lines)]
352 pub fn new(registry: &Registry) -> Self {
353 macro_rules! reg {
354 ($m:expr) => {{
355 let m = $m;
356 registry.register(Box::new(m.clone())).unwrap();
357 m
358 }};
359 }
360
361 let (events_ingested, events_emitted) = register_event_counters(registry);
362
363 Self {
364 #[cfg(feature = "cluster")]
365 cluster_subscription: ClusterSubscriptionMetrics::new(registry),
366 events_ingested,
367 events_emitted,
368 events_dropped: reg!(IntCounter::new("events_dropped_total", "Events dropped").unwrap()),
369 cycles: reg!(IntCounter::new(
370 "cycles_total",
371 "Normal processing cycles completed (excludes checkpoint graph drains)"
372 )
373 .unwrap()),
374 batches: reg!(IntCounter::new("batches_total", "Batches processed").unwrap()),
375 queries_compiled: reg!(IntCounter::new(
376 "queries_compiled_total",
377 "Queries using compiled PhysicalExpr"
378 )
379 .unwrap()),
380 queries_cached_plan: reg!(IntCounter::new(
381 "queries_cached_plan_total",
382 "Queries using cached logical plan"
383 )
384 .unwrap()),
385 cycles_backpressured: reg!(IntCounter::new(
386 "cycles_backpressured_total",
387 "Cycles skipped by backpressure"
388 )
389 .unwrap()),
390 mv_updates: reg!(
391 IntCounter::new("mv_updates_total", "Materialized view updates").unwrap()
392 ),
393 mv_bytes_stored: reg!(
394 IntGauge::new("mv_bytes_stored", "Approximate MV bytes stored").unwrap()
395 ),
396 pipeline_watermark: reg!(IntGauge::new(
397 "pipeline_watermark",
398 "Global pipeline watermark"
399 )
400 .unwrap()),
401 source_watermark_ms: reg!(IntGaugeVec::new(
403 Opts::new("source_watermark_ms", "Per-source watermark (epoch-ms)"),
404 &["source"],
405 )
406 .unwrap()),
407 source_idle: reg!(IntGaugeVec::new(
408 Opts::new(
409 "source_idle",
410 "1 if source idle (excluded from watermark min)"
411 ),
412 &["source"],
413 )
414 .unwrap()),
415 stream_watermark_ms: reg!(IntGaugeVec::new(
416 Opts::new("stream_watermark_ms", "Per-stream watermark (epoch-ms)"),
417 &["stream"],
418 )
419 .unwrap()),
420 input_buf_bytes: reg!(IntGaugeVec::new(
421 Opts::new("input_buf_bytes", "Per-stream input buffer bytes"),
422 &["stream"],
423 )
424 .unwrap()),
425 managed_state_accounted_bytes: reg!(IntGaugeVec::new(
427 Opts::new(
428 "managed_state_accounted_bytes",
429 "Operator-reported retained-state charge; current aggregate values are lower bounds between checkpoint/lifecycle reconciliation and exclude hash buckets, nested/shared payloads, allocator overhead, and RSS",
430 ),
431 &["operator", "phase"],
432 )
433 .unwrap()),
434 shed_records_total: reg!(IntCounterVec::new(
435 Opts::new("shed_records_total", "Rows shed by ShedOldest policy"),
436 &["stream"],
437 )
438 .unwrap()),
439 checkpoints_completed: reg!(IntCounter::new(
440 "checkpoints_completed_total",
441 "Completed checkpoints"
442 )
443 .unwrap()),
444 checkpoints_failed: reg!(IntCounter::new(
445 "checkpoints_failed_total",
446 "Failed checkpoints"
447 )
448 .unwrap()),
449 checkpoint_epoch: reg!(
450 IntGauge::new("checkpoint_epoch", "Current checkpoint epoch").unwrap()
451 ),
452 checkpoint_size_bytes: reg!(IntGauge::new(
453 "checkpoint_size_bytes",
454 "Last checkpoint size"
455 )
456 .unwrap()),
457 sink_write_failures: reg!(IntCounter::new(
458 "sink_write_failures_total",
459 "Sink write errors"
460 )
461 .unwrap()),
462 sink_write_timeouts: reg!(IntCounter::new(
463 "sink_write_timeouts_total",
464 "Sink write timeouts"
465 )
466 .unwrap()),
467 sink_task_channel_closed: reg!(IntCounter::new(
468 "sink_task_channel_closed_total",
469 "Sink task channel closed"
470 )
471 .unwrap()),
472 sink_filter_rejected_rows: reg!(IntCounterVec::new(
473 Opts::new(
474 "sink_filter_rejected_rows_total",
475 "Rows dropped because the sink filter failed to compile",
476 ),
477 &["sink"],
478 )
479 .unwrap()),
480 window_late_dropped: reg!(IntCounter::new(
481 "window_late_dropped_total",
482 "Window assignments dropped past allowed_lateness"
483 )
484 .unwrap()),
485 events_null_timestamp: reg!(IntCounter::new(
486 "events_null_timestamp_total",
487 "Source rows dropped because the event-time column was null"
488 )
489 .unwrap()),
490 temporal_filter_buffered: reg!(IntGauge::new(
491 "temporal_filter_buffered",
492 "Rows buffered by retracting temporal-filter operators"
493 )
494 .unwrap()),
495 temporal_filter_inserts: reg!(IntCounter::new(
496 "temporal_filter_inserts_total",
497 "Z-set inserts emitted by temporal-filter operators"
498 )
499 .unwrap()),
500 temporal_filter_retracts: reg!(IntCounter::new(
501 "temporal_filter_retracts_total",
502 "Z-set retractions emitted by temporal-filter operators"
503 )
504 .unwrap()),
505 temporal_filter_dropped: reg!(IntCounter::new(
506 "temporal_filter_dropped_total",
507 "Rows dropped un-emitted by temporal-filter operators"
508 )
509 .unwrap()),
510 cycle_duration: reg!(Histogram::with_opts(
511 HistogramOpts::new(
512 "cycle_duration_seconds",
513 "Normal processing-cycle duration (excludes checkpoint graph drains)",
514 )
515 .buckets(vec![
516 1e-7, 5e-7, 1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 5e-3, 1e-2, 5e-2,
517 1e-1, 5e-1, 1.0,
518 ]),
519 )
520 .unwrap()),
521 cycle_execute_duration: reg!(Histogram::with_opts(
522 HistogramOpts::new(
523 "cycle_execute_duration_seconds",
524 "SQL operator-graph execution within successful normal processing cycles",
525 )
526 .buckets(vec![
527 1e-7, 5e-7, 1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 5e-3, 1e-2, 5e-2,
528 1e-1, 5e-1, 1.0,
529 ]),
530 )
531 .unwrap()),
532 cycle_output_store_duration: reg!(Histogram::with_opts(
533 HistogramOpts::new(
534 "cycle_output_store_duration_seconds",
535 "Materialized-view and subscription-stream publication within successful normal processing cycles",
536 )
537 .buckets(vec![
538 1e-7, 5e-7, 1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 5e-3, 1e-2, 5e-2,
539 1e-1, 5e-1, 1.0,
540 ]),
541 )
542 .unwrap()),
543 cycle_sink_enqueue_duration: reg!(Histogram::with_opts(
544 HistogramOpts::new(
545 "cycle_sink_enqueue_duration_seconds",
546 "Sink publication preparation and command admission within successful normal processing cycles",
547 )
548 .buckets(vec![
549 1e-7, 5e-7, 1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 5e-3, 1e-2, 5e-2,
550 1e-1, 5e-1, 1.0,
551 ]),
552 )
553 .unwrap()),
554 operator_process_duration: reg!(HistogramVec::new(
556 HistogramOpts::new(
557 "operator_process_duration_seconds",
558 "Operator processing duration by catalog operator and execution mode",
559 )
560 .buckets(prometheus::exponential_buckets(0.0001, 4.0, 10).unwrap()),
561 &["operator", "mode"],
562 )
563 .unwrap()),
564 checkpoint_duration: reg!(Histogram::with_opts(
567 HistogramOpts::new("checkpoint_duration_seconds", "Checkpoint cycle duration")
568 .buckets(prometheus::exponential_buckets(0.01, 2.0, 15).unwrap()),
569 )
570 .unwrap()),
571 checkpoint_state_capture_duration: reg!(Histogram::with_opts(
572 HistogramOpts::new(
573 "checkpoint_state_capture_duration_seconds",
574 "Synchronous mutable checkpoint-state capture duration",
575 )
576 .buckets(prometheus::exponential_buckets(0.001, 2.0, 16).unwrap()),
577 )
578 .unwrap()),
579 checkpoint_pipeline_stall_duration: reg!(Histogram::with_opts(
582 HistogramOpts::new(
583 "checkpoint_pipeline_stall_duration_seconds",
584 "Pipeline stall per checkpoint barrier (align + capture + resume gate)",
585 )
586 .buckets(prometheus::exponential_buckets(0.001, 2.0, 16).unwrap()),
587 )
588 .unwrap()),
589 checkpoint_barrier_local_duration: reg!(Histogram::with_opts(
590 HistogramOpts::new(
591 "checkpoint_barrier_local_duration_seconds",
592 "Local paused barrier work (sink fence + shuffle align + state capture + tail handoff)",
593 )
594 .buckets(prometheus::exponential_buckets(0.001, 2.0, 16).unwrap()),
595 )
596 .unwrap()),
597 checkpoint_aligned_resume_wait: reg!(Histogram::with_opts(
598 HistogramOpts::new(
599 "checkpoint_aligned_resume_wait_seconds",
600 "Cluster-shuffle pause waiting for the global Aligned release",
601 )
602 .buckets(prometheus::exponential_buckets(0.001, 2.0, 16).unwrap()),
603 )
604 .unwrap()),
605 sink_precommit_duration: reg!(Histogram::with_opts(
607 HistogramOpts::new("sink_precommit_duration_seconds", "Sink pre-commit latency")
608 .buckets(prometheus::exponential_buckets(0.005, 2.0, 16).unwrap()),
609 )
610 .unwrap()),
611 lookup_cache_hits: reg!(IntCounterVec::new(
613 Opts::new("lookup_cache_hits_total", "On-demand lookup cache hits"),
614 &["table"],
615 )
616 .unwrap()),
617 lookup_cache_misses: reg!(IntCounterVec::new(
618 Opts::new("lookup_cache_misses_total", "On-demand lookup cache misses"),
619 &["table"],
620 )
621 .unwrap()),
622 lookup_source_errors: reg!(IntCounterVec::new(
623 Opts::new(
624 "lookup_source_errors_total",
625 "On-demand lookup source fetch errors"
626 ),
627 &["table"],
628 )
629 .unwrap()),
630 lookup_in_flight_rows: reg!(IntGaugeVec::new(
631 Opts::new(
632 "lookup_in_flight_rows",
633 "On-demand lookup rows awaiting a source fetch"
634 ),
635 &["table"],
636 )
637 .unwrap()),
638 placement_vnodes_per_domain: reg!(IntGaugeVec::new(
639 Opts::new(
640 "placement_vnodes_per_domain",
641 "Vnodes owned per failure domain (cluster mode)"
642 ),
643 &["domain"],
644 )
645 .unwrap()),
646 placement_blast_radius_ratio: reg!(Gauge::new(
647 "placement_blast_radius_ratio",
648 "Largest single domain's share of all vnodes (0-1); state affected if it fails"
649 )
650 .unwrap()),
651 pipeline_faults_total: reg!(IntCounter::new(
652 "pipeline_faults_total",
653 "Compute-thread crashes that faulted the pipeline"
654 )
655 .unwrap()),
656 pipeline_cycle_errors_total: reg!(IntCounter::new(
657 "pipeline_cycle_errors_total",
658 "Fatal cycle errors dropped-and-continued under at-least-once delivery"
659 )
660 .unwrap()),
661 pipeline_restarts_total: reg!(IntCounter::new(
662 "pipeline_restarts_total",
663 "Automatic recover-from-checkpoint restarts after a pipeline fault"
664 )
665 .unwrap()),
666 coordinated_recoveries_total: reg!(IntCounter::new(
667 "coordinated_recoveries_total",
668 "Leader-coordinated global restart-to-epoch rounds applied by this node"
669 )
670 .unwrap()),
671 coordinated_recovery_failures_total: reg!(IntCounter::new(
672 "coordinated_recovery_failures_total",
673 "Leader-coordinated recovery rounds abandoned (self-restore failed or quorum timed out)"
674 )
675 .unwrap()),
676 shuffle_delivery_loss_incidents_total: reg!(IntCounter::new(
677 "shuffle_delivery_loss_incidents_total",
678 "Cross-node shuffle delivery-loss incidents (fences the epoch, forces replay)"
679 )
680 .unwrap()),
681 }
682 }
683}
684
685#[cfg(all(test, feature = "cluster"))]
686mod tests {
687 use super::*;
688
689 #[test]
690 fn cluster_subscription_metric_family_has_the_release_names() {
691 let registry = Registry::new();
692 let _metrics = EngineMetrics::new(®istry);
693 let names = registry
694 .gather()
695 .into_iter()
696 .map(|family| family.name().to_owned())
697 .collect::<std::collections::BTreeSet<_>>();
698 for expected in [
699 "cluster_subscription_active_readers",
700 "cluster_subscription_open_total",
701 "cluster_subscription_open_failures_total",
702 "cluster_subscription_frames_committed_total",
703 "cluster_subscription_rows_committed_total",
704 "cluster_subscription_bytes_committed_total",
705 "cluster_subscription_segments_written_total",
706 "cluster_subscription_segment_write_failures_total",
707 "cluster_subscription_manifest_failures_total",
708 "cluster_subscription_integrity_failures_total",
709 "cluster_subscription_stale_writer_rejections_total",
710 "cluster_subscription_sequence_gaps_total",
711 "cluster_subscription_replay_bytes_total",
712 "cluster_subscription_replay_frames_total",
713 "cluster_subscription_replay_pruned_total",
714 "cluster_subscription_gateway_lag_disconnects_total",
715 "cluster_subscription_pending_bytes",
716 "cluster_subscription_retained_bytes",
717 "cluster_subscription_orphan_bytes",
718 "cluster_subscription_checkpoint_prepare_seconds",
719 "cluster_subscription_commit_visibility_seconds",
720 "cluster_subscription_gateway_manifest_refresh_seconds",
721 ] {
722 assert!(names.contains(expected), "missing metric {expected}");
723 }
724 }
725}