Skip to main content

laminar_db/
engine_metrics.rs

1//! Prometheus metrics for the streaming engine.
2
3use prometheus::{
4    Histogram, HistogramOpts, HistogramVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Opts,
5    Registry,
6};
7
8/// Bounded-cardinality metrics for committed cluster subscriptions.
9#[cfg(feature = "cluster")]
10pub struct ClusterSubscriptionMetrics {
11    /// Readers currently attached to this gateway process.
12    pub active_readers: IntGauge,
13    /// Cluster subscription open attempts handled by this process.
14    pub open_total: IntCounter,
15    /// Failed cluster subscription open attempts.
16    pub open_failures_total: IntCounter,
17    /// Output frames made visible by authoritative checkpoint Commit outcomes.
18    pub frames_committed_total: IntCounter,
19    /// Output rows made visible by authoritative checkpoint Commit outcomes.
20    pub rows_committed_total: IntCounter,
21    /// Encoded segment bytes made visible by authoritative checkpoint Commit outcomes.
22    pub bytes_committed_total: IntCounter,
23    /// Immutable output segments written by this process.
24    pub segments_written_total: IntCounter,
25    /// Immutable output segment writes that failed.
26    pub segment_write_failures_total: IntCounter,
27    /// Manifest construction, loading, or validation failures.
28    pub manifest_failures_total: IntCounter,
29    /// Committed manifest or segment integrity failures.
30    pub integrity_failures_total: IntCounter,
31    /// Fenced output writer attempts rejected by this process.
32    pub stale_writer_rejections_total: IntCounter,
33    /// Partition continuity failures detected by writers or readers.
34    pub sequence_gaps_total: IntCounter,
35    /// Encoded bytes read during committed replay.
36    pub replay_bytes_total: IntCounter,
37    /// Frames read during committed replay.
38    pub replay_frames_total: IntCounter,
39    /// Replay opens rejected because their epoch was pruned.
40    pub replay_pruned_total: IntCounter,
41    /// Readers disconnected after exhausting bounded gateway lag allowance.
42    pub gateway_lag_disconnects_total: IntCounter,
43    /// In-memory output bytes awaiting checkpoint disposition on this process.
44    pub pending_bytes: IntGauge,
45    /// Bytes reachable from the retained committed output-log suffix.
46    pub retained_bytes: IntGauge,
47    /// Grace-held unreachable output bytes found by the latest orphan scan.
48    pub orphan_bytes: IntGauge,
49    /// Subscription output encoding and upload duration during checkpoint preparation.
50    pub checkpoint_prepare_seconds: Histogram,
51    /// Authoritative Commit publication duration before local visibility.
52    pub commit_visibility_seconds: Histogram,
53    /// Gateway committed-index refresh duration.
54    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
175/// Materialized view results and the freshness of committed view reads.
176pub struct ViewMetrics {
177    /// Materialized view updates.
178    pub updates: IntCounter,
179    /// Approximate MV bytes stored.
180    pub bytes_stored: IntGauge,
181    /// Age of the newest committed checkpoint, which committed view reads observe.
182    pub committed_staleness_ms: IntGauge,
183    /// Times a view's committed reads became staler than its declared freshness. Label: `view`.
184    pub freshness_breaches_total: IntCounterVec,
185    /// Rows append-only views dropped at ingest because they arrived behind the event-time
186    /// retention horizon. A row-cap retention has no horizon and never drops here.
187    pub rows_below_horizon_total: IntCounter,
188    /// Ad-hoc reads of views. Label: `level` (`latest`, `committed` or `recent`).
189    pub reads_total: IntCounterVec,
190    /// Age of the committed checkpoint each committed or recent read observed, in seconds.
191    pub committed_read_age_seconds: Histogram,
192    /// How long each recent read waited for a committed checkpoint holding what its sources had
193    /// upstream when it arrived, in seconds.
194    pub recent_read_wait_seconds: Histogram,
195    /// Times a source could not report its upstream position to a recent read. Label: `source`.
196    pub recency_probe_failures_total: IntCounterVec,
197}
198
199impl ViewMetrics {
200    fn new(registry: &Registry) -> Self {
201        let metrics = Self {
202            updates: IntCounter::new("mv_updates_total", "Materialized view updates").unwrap(),
203            bytes_stored: IntGauge::new("mv_bytes_stored", "Approximate MV bytes stored").unwrap(),
204            committed_staleness_ms: IntGauge::new(
205                "mv_committed_staleness_ms",
206                "Age of the newest committed checkpoint that committed view reads observe",
207            )
208            .unwrap(),
209            freshness_breaches_total: IntCounterVec::new(
210                Opts::new(
211                    "mv_freshness_breaches_total",
212                    "Times committed view reads became staler than the view's declared freshness",
213                ),
214                &["view"],
215            )
216            .unwrap(),
217            rows_below_horizon_total: IntCounter::new(
218                "mv_rows_below_horizon_total",
219                "Rows append-only views dropped because they arrived behind the retention horizon",
220            )
221            .unwrap(),
222            reads_total: IntCounterVec::new(
223                Opts::new(
224                    "mv_reads_total",
225                    "Ad-hoc reads of materialized views by read level",
226                ),
227                &["level"],
228            )
229            .unwrap(),
230            committed_read_age_seconds: Histogram::with_opts(
231                HistogramOpts::new(
232                    "mv_committed_read_age_seconds",
233                    "Age of the committed checkpoint each committed view read observed",
234                )
235                .buckets(vec![0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0]),
236            )
237            .unwrap(),
238            recent_read_wait_seconds: Histogram::with_opts(
239                HistogramOpts::new(
240                    "mv_recent_read_wait_seconds",
241                    "Time each recent view read waited for a committed checkpoint holding what \
242                     its sources had upstream when it arrived",
243                )
244                .buckets(vec![0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0]),
245            )
246            .unwrap(),
247            recency_probe_failures_total: IntCounterVec::new(
248                Opts::new(
249                    "mv_recency_probe_failures_total",
250                    "Times a source could not report its upstream position to a recent view read",
251                ),
252                &["source"],
253            )
254            .unwrap(),
255        };
256        registry
257            .register(Box::new(metrics.updates.clone()))
258            .unwrap();
259        registry
260            .register(Box::new(metrics.bytes_stored.clone()))
261            .unwrap();
262        registry
263            .register(Box::new(metrics.committed_staleness_ms.clone()))
264            .unwrap();
265        registry
266            .register(Box::new(metrics.freshness_breaches_total.clone()))
267            .unwrap();
268        registry
269            .register(Box::new(metrics.rows_below_horizon_total.clone()))
270            .unwrap();
271        registry
272            .register(Box::new(metrics.reads_total.clone()))
273            .unwrap();
274        registry
275            .register(Box::new(metrics.committed_read_age_seconds.clone()))
276            .unwrap();
277        registry
278            .register(Box::new(metrics.recent_read_wait_seconds.clone()))
279            .unwrap();
280        registry
281            .register(Box::new(metrics.recency_probe_failures_total.clone()))
282            .unwrap();
283        metrics
284    }
285
286    /// Count one read at `level`, at a committed cut taken at `taken_at_ms` or at the latest
287    /// cycle.
288    pub(crate) fn observe_read(&self, level: &str, taken_at_ms: Option<u64>) {
289        self.reads_total.with_label_values(&[level]).inc();
290        let Some(taken_at_ms) = taken_at_ms else {
291            return;
292        };
293        let now_ms = std::time::SystemTime::now()
294            .duration_since(std::time::UNIX_EPOCH)
295            .map_or(0, |elapsed| {
296                u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX)
297            });
298        #[allow(clippy::cast_precision_loss)] // ages are far below 2^52 ms
299        self.committed_read_age_seconds
300            .observe(now_ms.saturating_sub(taken_at_ms) as f64 / 1_000.0);
301    }
302}
303
304/// Vnode placement as this process last adopted it.
305#[cfg(feature = "cluster")]
306pub struct PlacementMetrics {
307    /// Vnodes owned per failure domain. Label: `domain`.
308    pub vnodes_per_domain: IntGaugeVec,
309    /// Largest single domain's share of all vnodes (`[0, 1]`) — the blast radius.
310    pub blast_radius_ratio: prometheus::Gauge,
311    /// Vnodes owned by this process.
312    pub owned_vnodes: IntGauge,
313}
314
315#[cfg(feature = "cluster")]
316impl PlacementMetrics {
317    fn new(registry: &Registry) -> Self {
318        let vnodes_per_domain = IntGaugeVec::new(
319            Opts::new(
320                "placement_vnodes_per_domain",
321                "Vnodes owned per failure domain (cluster mode)",
322            ),
323            &["domain"],
324        )
325        .unwrap();
326        registry
327            .register(Box::new(vnodes_per_domain.clone()))
328            .unwrap();
329        let blast_radius_ratio = prometheus::Gauge::new(
330            "placement_blast_radius_ratio",
331            "Largest single domain's share of all vnodes (0-1); state affected if it fails",
332        )
333        .unwrap();
334        registry
335            .register(Box::new(blast_radius_ratio.clone()))
336            .unwrap();
337        Self {
338            vnodes_per_domain,
339            blast_radius_ratio,
340            owned_vnodes: register_int_gauge(
341                registry,
342                "placement_owned_vnodes",
343                "Vnodes owned by this process (cluster mode)",
344            ),
345        }
346    }
347}
348
349fn register_int_counter(registry: &Registry, name: &str, help: &str) -> IntCounter {
350    let metric = IntCounter::new(name, help).unwrap();
351    registry.register(Box::new(metric.clone())).unwrap();
352    metric
353}
354
355fn register_event_counters(registry: &Registry) -> (IntCounter, IntCounter) {
356    (
357        register_int_counter(
358            registry,
359            "events_ingested_total",
360            "Events ingested from sources",
361        ),
362        register_int_counter(
363            registry,
364            "events_emitted_total",
365            "Events emitted to streams",
366        ),
367    )
368}
369
370fn register_operator_process_duration(registry: &Registry) -> HistogramVec {
371    // Operator names are catalog-bound and mode has exactly two values.
372    let metric = HistogramVec::new(
373        HistogramOpts::new(
374            "operator_process_duration_seconds",
375            "Operator processing duration by catalog operator and execution mode",
376        )
377        .buckets(prometheus::exponential_buckets(0.0001, 4.0, 10).unwrap()),
378        &["operator", "mode"],
379    )
380    .unwrap();
381    registry.register(Box::new(metric.clone())).unwrap();
382    metric
383}
384
385#[cfg(feature = "cluster")]
386fn register_int_gauge(registry: &Registry, name: &str, help: &str) -> IntGauge {
387    let metric = IntGauge::new(name, help).unwrap();
388    registry.register(Box::new(metric.clone())).unwrap();
389    metric
390}
391
392#[cfg(feature = "cluster")]
393fn register_subscription_histogram(registry: &Registry, name: &str, help: &str) -> Histogram {
394    let metric = Histogram::with_opts(
395        HistogramOpts::new(name, help)
396            .buckets(prometheus::exponential_buckets(0.001, 2.0, 18).unwrap()),
397    )
398    .unwrap();
399    registry.register(Box::new(metric.clone())).unwrap();
400    metric
401}
402
403/// Pipeline metrics registered on an explicit prometheus `Registry`.
404///
405/// Constructed once at startup, `Arc`-shared into `PipelineCallback`,
406/// `CheckpointCoordinator`, and `OperatorGraph`.
407pub struct EngineMetrics {
408    pub(crate) flow: crate::flow_metrics::FlowMetrics,
409    /// Committed cluster-subscription metrics without per-stream or per-reader labels.
410    #[cfg(feature = "cluster")]
411    pub cluster_subscription: ClusterSubscriptionMetrics,
412    /// Events ingested from sources.
413    pub events_ingested: IntCounter,
414    /// Events emitted to streams.
415    pub events_emitted: IntCounter,
416    /// Events dropped.
417    pub events_dropped: IntCounter,
418    /// Processing cycles completed.
419    pub cycles: IntCounter,
420    /// Batches processed.
421    pub batches: IntCounter,
422    /// Queries using compiled `PhysicalExpr`.
423    pub queries_compiled: IntCounter,
424    /// Queries using cached logical plan.
425    pub queries_cached_plan: IntCounter,
426    /// Cycles whose source intake waited for graph port room.
427    pub cycles_backpressured: IntCounter,
428    /// Materialized view results and committed reads.
429    pub views: ViewMetrics,
430    /// Global pipeline watermark.
431    pub pipeline_watermark: IntGauge,
432    /// Per-source watermark (epoch-ms). Label: `source`.
433    pub source_watermark_ms: IntGaugeVec,
434    /// `1` if a source is idle (excluded from the watermark min), else
435    /// `0`. Label: `source`.
436    pub source_idle: IntGaugeVec,
437    /// Per-stream watermark (epoch-ms). Label: `stream`.
438    pub stream_watermark_ms: IntGaugeVec,
439    /// Per-stream input-port buffered bytes. Label: `stream`.
440    pub input_buf_bytes: IntGaugeVec,
441    /// Operator-reported retained managed-state charge. Labels: `operator`, `phase`.
442    /// `live` is current at sampling; transient `prepared`/`retired` values are the maximum
443    /// observed since the prior sample. Current aggregate values are lower bounds between cold
444    /// reconciliation points and exclude hash buckets, nested/shared payloads, allocator overhead,
445    /// and process RSS.
446    pub managed_state_accounted_bytes: IntGaugeVec,
447    /// Per-stream rows shed by the `ShedOldest` policy. Label: `stream`.
448    pub shed_records_total: IntCounterVec,
449    /// Completed checkpoints.
450    pub checkpoints_completed: IntCounter,
451    /// Failed checkpoints.
452    pub checkpoints_failed: IntCounter,
453    /// Current checkpoint epoch.
454    pub checkpoint_epoch: IntGauge,
455    /// Last checkpoint size in bytes.
456    pub checkpoint_size_bytes: IntGauge,
457    /// Sink write errors.
458    pub sink_write_failures: IntCounter,
459    /// Sink write timeouts.
460    pub sink_write_timeouts: IntCounter,
461    /// Sink task channel closed.
462    pub sink_task_channel_closed: IntCounter,
463    /// Rows dropped because the sink's WHERE filter failed to compile to
464    /// a `PhysicalExpr` (fail-closed). Label: `sink`.
465    pub sink_filter_rejected_rows: IntCounterVec,
466    /// Window assignments dropped past `allowed_lateness`. A hopping-window row can contribute
467    /// more than one assignment.
468    pub window_late_dropped: IntCounter,
469    /// Source rows dropped because the event-time column was null.
470    pub events_null_timestamp: IntCounter,
471    /// Rows currently buffered by temporal-filter operators.
472    pub temporal_filter_buffered: IntGauge,
473    /// Z-set inserts (+1) emitted by temporal-filter operators.
474    pub temporal_filter_inserts: IntCounter,
475    /// Z-set retractions (-1) emitted by temporal-filter operators.
476    pub temporal_filter_retracts: IntCounter,
477    /// Late / born-expired / beyond-horizon rows dropped un-emitted.
478    pub temporal_filter_dropped: IntCounter,
479    /// Per-cycle processing duration.
480    pub cycle_duration: Histogram,
481    /// SQL operator-graph execution within successful normal cycles.
482    pub cycle_execute_duration: Histogram,
483    /// Materialized-view and subscription-stream publication within successful normal cycles.
484    pub cycle_output_store_duration: Histogram,
485    /// Sink command admission within successful normal cycles.
486    pub cycle_sink_enqueue_duration: Histogram,
487    /// Per-operator processing duration. Labels: `operator`, `mode` (`normal` or
488    /// `checkpoint_drain`).
489    pub operator_process_duration: HistogramVec,
490    /// Checkpoint cycle duration.
491    pub checkpoint_duration: Histogram,
492    /// Synchronous mutable checkpoint-state capture duration.
493    pub checkpoint_state_capture_duration: Histogram,
494    /// Pipeline stall per barrier: sink write fence, shuffle alignment, state capture, durable-tail
495    /// handoff, and the Aligned resume gate. Every durable tail remains supervised in the
496    /// background; committable sink writes wait on its exact successor-epoch gate.
497    pub checkpoint_pipeline_stall_duration: Histogram,
498    /// Local barrier work while the pipeline is paused: sink fencing, shuffle alignment, state
499    /// capture, and construction of the immutable durable-tail handoff.
500    pub checkpoint_barrier_local_duration: Histogram,
501    /// Cluster-shuffle pause after local capture while waiting for the global Aligned release.
502    /// Embedded and single-node runtimes do not observe this metric.
503    pub checkpoint_aligned_resume_wait: Histogram,
504    /// Waves the checkpoint's distributed shuffle flush ran before every participant was idle.
505    /// Embedded and single-node runtimes do not observe this metric.
506    pub checkpoint_shuffle_flush_waves: Histogram,
507    /// Sink pre-commit round-trip (2PC phase 1).
508    pub sink_precommit_duration: Histogram,
509    /// On-demand lookup cache hits (served without a source fetch). Label: `table`.
510    pub lookup_cache_hits: IntCounterVec,
511    /// On-demand lookup cache misses (not in cache). Label: `table`.
512    pub lookup_cache_misses: IntCounterVec,
513    /// On-demand lookup source fetch errors/timeouts. Label: `table`.
514    pub lookup_source_errors: IntCounterVec,
515    /// On-demand lookup rows awaiting a source fetch. Label: `table`.
516    pub lookup_in_flight_rows: IntGaugeVec,
517    /// Vnode placement as this process last adopted it.
518    #[cfg(feature = "cluster")]
519    pub placement: PlacementMetrics,
520    /// Unexpected compute-thread exits (operator panics) that faulted the pipeline.
521    pub pipeline_faults_total: IntCounter,
522    /// Fatal cycle errors dropped-and-continued under at-least-once delivery.
523    pub pipeline_cycle_errors_total: IntCounter,
524    /// Automatic recover-from-checkpoint restarts triggered by the fault supervisor.
525    pub pipeline_restarts_total: IntCounter,
526    /// Leader-coordinated global restart-to-epoch rounds this node applied (cluster mode).
527    pub coordinated_recoveries_total: IntCounter,
528    /// Leader-coordinated recovery rounds abandoned (self-restore failed or restore quorum timed out).
529    pub coordinated_recovery_failures_total: IntCounter,
530    /// Shuffle delivery-loss incidents; each one fences an epoch and forces replay.
531    pub shuffle_delivery_loss_incidents_total: IntCounter,
532    /// Batches on a colocated aggregate input that held rows owned elsewhere and were
533    /// repartitioned; only expected around a vnode transition.
534    pub colocated_routing_fallbacks_total: IntCounter,
535}
536
537impl EngineMetrics {
538    /// Register all engine metrics on the given registry. Startup only.
539    ///
540    /// # Panics
541    ///
542    /// Panics if metric registration fails (duplicate names).
543    #[must_use]
544    #[allow(clippy::too_many_lines)]
545    pub fn new(registry: &Registry) -> Self {
546        macro_rules! reg {
547            ($m:expr) => {{
548                let m = $m;
549                registry.register(Box::new(m.clone())).unwrap();
550                m
551            }};
552        }
553
554        let (events_ingested, events_emitted) = register_event_counters(registry);
555
556        Self {
557            flow: crate::flow_metrics::FlowMetrics::new(registry),
558            #[cfg(feature = "cluster")]
559            cluster_subscription: ClusterSubscriptionMetrics::new(registry),
560            events_ingested,
561            events_emitted,
562            events_dropped: reg!(IntCounter::new("events_dropped_total", "Events dropped").unwrap()),
563            cycles: reg!(IntCounter::new(
564                "cycles_total",
565                "Normal processing cycles completed (excludes checkpoint graph drains)"
566            )
567            .unwrap()),
568            batches: reg!(IntCounter::new("batches_total", "Batches processed").unwrap()),
569            queries_compiled: reg!(IntCounter::new(
570                "queries_compiled_total",
571                "Queries using compiled PhysicalExpr"
572            )
573            .unwrap()),
574            queries_cached_plan: reg!(IntCounter::new(
575                "queries_cached_plan_total",
576                "Queries using cached logical plan"
577            )
578            .unwrap()),
579            cycles_backpressured: reg!(IntCounter::new(
580                "cycles_backpressured_total",
581                "Cycles whose source intake waited for graph port room"
582            )
583            .unwrap()),
584            views: ViewMetrics::new(registry),
585            pipeline_watermark: reg!(IntGauge::new(
586                "pipeline_watermark",
587                "Global pipeline watermark"
588            )
589            .unwrap()),
590            // Labels are catalog-bound, so cardinality is finite.
591            source_watermark_ms: reg!(IntGaugeVec::new(
592                Opts::new("source_watermark_ms", "Per-source watermark (epoch-ms)"),
593                &["source"],
594            )
595            .unwrap()),
596            source_idle: reg!(IntGaugeVec::new(
597                Opts::new(
598                    "source_idle",
599                    "1 if source idle (excluded from watermark min)"
600                ),
601                &["source"],
602            )
603            .unwrap()),
604            stream_watermark_ms: reg!(IntGaugeVec::new(
605                Opts::new("stream_watermark_ms", "Per-stream watermark (epoch-ms)"),
606                &["stream"],
607            )
608            .unwrap()),
609            input_buf_bytes: reg!(IntGaugeVec::new(
610                Opts::new("input_buf_bytes", "Per-stream input buffer bytes"),
611                &["stream"],
612            )
613            .unwrap()),
614            // Operator names are catalog-bound and phase is one of live/prepared/retired.
615            managed_state_accounted_bytes: reg!(IntGaugeVec::new(
616                Opts::new(
617                    "managed_state_accounted_bytes",
618                    "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",
619                ),
620                &["operator", "phase"],
621            )
622            .unwrap()),
623            shed_records_total: reg!(IntCounterVec::new(
624                Opts::new("shed_records_total", "Rows shed by ShedOldest policy"),
625                &["stream"],
626            )
627            .unwrap()),
628            checkpoints_completed: reg!(IntCounter::new(
629                "checkpoints_completed_total",
630                "Completed checkpoints"
631            )
632            .unwrap()),
633            checkpoints_failed: reg!(IntCounter::new(
634                "checkpoints_failed_total",
635                "Failed checkpoints"
636            )
637            .unwrap()),
638            checkpoint_epoch: reg!(
639                IntGauge::new("checkpoint_epoch", "Current checkpoint epoch").unwrap()
640            ),
641            checkpoint_size_bytes: reg!(IntGauge::new(
642                "checkpoint_size_bytes",
643                "Last checkpoint size"
644            )
645            .unwrap()),
646            sink_write_failures: reg!(IntCounter::new(
647                "sink_write_failures_total",
648                "Sink write errors"
649            )
650            .unwrap()),
651            sink_write_timeouts: reg!(IntCounter::new(
652                "sink_write_timeouts_total",
653                "Sink write timeouts"
654            )
655            .unwrap()),
656            sink_task_channel_closed: reg!(IntCounter::new(
657                "sink_task_channel_closed_total",
658                "Sink task channel closed"
659            )
660            .unwrap()),
661            sink_filter_rejected_rows: reg!(IntCounterVec::new(
662                Opts::new(
663                    "sink_filter_rejected_rows_total",
664                    "Rows dropped because the sink filter failed to compile",
665                ),
666                &["sink"],
667            )
668            .unwrap()),
669            window_late_dropped: reg!(IntCounter::new(
670                "window_late_dropped_total",
671                "Window assignments dropped past allowed_lateness"
672            )
673            .unwrap()),
674            events_null_timestamp: reg!(IntCounter::new(
675                "events_null_timestamp_total",
676                "Source rows dropped because the event-time column was null"
677            )
678            .unwrap()),
679            temporal_filter_buffered: reg!(IntGauge::new(
680                "temporal_filter_buffered",
681                "Rows buffered by retracting temporal-filter operators"
682            )
683            .unwrap()),
684            temporal_filter_inserts: reg!(IntCounter::new(
685                "temporal_filter_inserts_total",
686                "Z-set inserts emitted by temporal-filter operators"
687            )
688            .unwrap()),
689            temporal_filter_retracts: reg!(IntCounter::new(
690                "temporal_filter_retracts_total",
691                "Z-set retractions emitted by temporal-filter operators"
692            )
693            .unwrap()),
694            temporal_filter_dropped: reg!(IntCounter::new(
695                "temporal_filter_dropped_total",
696                "Rows dropped un-emitted by temporal-filter operators"
697            )
698            .unwrap()),
699            cycle_duration: reg!(Histogram::with_opts(
700                HistogramOpts::new(
701                    "cycle_duration_seconds",
702                    "Normal processing-cycle duration (excludes checkpoint graph drains)",
703                )
704                .buckets(vec![
705                        1e-7, 5e-7, 1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 5e-3, 1e-2, 5e-2,
706                        1e-1, 5e-1, 1.0,
707                    ]),
708            )
709            .unwrap()),
710            cycle_execute_duration: reg!(Histogram::with_opts(
711                HistogramOpts::new(
712                    "cycle_execute_duration_seconds",
713                    "SQL operator-graph execution within successful normal processing cycles",
714                )
715                .buckets(vec![
716                    1e-7, 5e-7, 1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 5e-3, 1e-2, 5e-2,
717                    1e-1, 5e-1, 1.0,
718                ]),
719            )
720            .unwrap()),
721            cycle_output_store_duration: reg!(Histogram::with_opts(
722                HistogramOpts::new(
723                    "cycle_output_store_duration_seconds",
724                    "Materialized-view and subscription-stream publication within successful normal processing cycles",
725                )
726                .buckets(vec![
727                    1e-7, 5e-7, 1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 5e-3, 1e-2, 5e-2,
728                    1e-1, 5e-1, 1.0,
729                ]),
730            )
731            .unwrap()),
732            cycle_sink_enqueue_duration: reg!(Histogram::with_opts(
733                HistogramOpts::new(
734                    "cycle_sink_enqueue_duration_seconds",
735                    "Sink publication preparation and command admission within successful normal processing cycles",
736                )
737                .buckets(vec![
738                    1e-7, 5e-7, 1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 5e-3, 1e-2, 5e-2,
739                    1e-1, 5e-1, 1.0,
740                ]),
741            )
742            .unwrap()),
743            operator_process_duration: register_operator_process_duration(registry),
744            // Checkpoint: serialization_timeout=120s, so max bucket must cover that.
745            // 0.01 * 2^14 = 163.84s.
746            checkpoint_duration: reg!(Histogram::with_opts(
747                HistogramOpts::new("checkpoint_duration_seconds", "Checkpoint cycle duration")
748                    .buckets(prometheus::exponential_buckets(0.01, 2.0, 15).unwrap()),
749            )
750            .unwrap()),
751            checkpoint_state_capture_duration: reg!(Histogram::with_opts(
752                HistogramOpts::new(
753                    "checkpoint_state_capture_duration_seconds",
754                    "Synchronous mutable checkpoint-state capture duration",
755                )
756                .buckets(prometheus::exponential_buckets(0.001, 2.0, 16).unwrap()),
757            )
758            .unwrap()),
759            // Stall target is sub-second; the resume gate is bounded at
760            // 30s. 0.001 * 2^15 = 32.77s.
761            checkpoint_pipeline_stall_duration: reg!(Histogram::with_opts(
762                HistogramOpts::new(
763                    "checkpoint_pipeline_stall_duration_seconds",
764                    "Pipeline stall per checkpoint barrier (align + capture + resume gate)",
765                )
766                .buckets(prometheus::exponential_buckets(0.001, 2.0, 16).unwrap()),
767            )
768            .unwrap()),
769            checkpoint_barrier_local_duration: reg!(Histogram::with_opts(
770                HistogramOpts::new(
771                    "checkpoint_barrier_local_duration_seconds",
772                    "Local paused barrier work (sink fence + shuffle align + state capture + tail handoff)",
773                )
774                .buckets(prometheus::exponential_buckets(0.001, 2.0, 16).unwrap()),
775            )
776            .unwrap()),
777            checkpoint_aligned_resume_wait: reg!(Histogram::with_opts(
778                HistogramOpts::new(
779                    "checkpoint_aligned_resume_wait_seconds",
780                    "Cluster-shuffle pause waiting for the global Aligned release",
781                )
782                .buckets(prometheus::exponential_buckets(0.001, 2.0, 16).unwrap()),
783            )
784            .unwrap()),
785            checkpoint_shuffle_flush_waves: reg!(Histogram::with_opts(
786                HistogramOpts::new(
787                    "checkpoint_shuffle_flush_waves",
788                    "Waves of the checkpoint's distributed shuffle flush",
789                )
790                .buckets(prometheus::linear_buckets(1.0, 1.0, 16).unwrap()),
791            )
792            .unwrap()),
793            // The one attempt deadline defaults to 120s. 0.005 * 2^15 = 163.84s.
794            sink_precommit_duration: reg!(Histogram::with_opts(
795                HistogramOpts::new("sink_precommit_duration_seconds", "Sink pre-commit latency")
796                    .buckets(prometheus::exponential_buckets(0.005, 2.0, 16).unwrap()),
797            )
798            .unwrap()),
799            // Labels are bound to the registered lookup tables, so cardinality is finite.
800            lookup_cache_hits: reg!(IntCounterVec::new(
801                Opts::new("lookup_cache_hits_total", "On-demand lookup cache hits"),
802                &["table"],
803            )
804            .unwrap()),
805            lookup_cache_misses: reg!(IntCounterVec::new(
806                Opts::new("lookup_cache_misses_total", "On-demand lookup cache misses"),
807                &["table"],
808            )
809            .unwrap()),
810            lookup_source_errors: reg!(IntCounterVec::new(
811                Opts::new(
812                    "lookup_source_errors_total",
813                    "On-demand lookup source fetch errors"
814                ),
815                &["table"],
816            )
817            .unwrap()),
818            lookup_in_flight_rows: reg!(IntGaugeVec::new(
819                Opts::new(
820                    "lookup_in_flight_rows",
821                    "On-demand lookup rows awaiting a source fetch"
822                ),
823                &["table"],
824            )
825            .unwrap()),
826            #[cfg(feature = "cluster")]
827            placement: PlacementMetrics::new(registry),
828            pipeline_faults_total: reg!(IntCounter::new(
829                "pipeline_faults_total",
830                "Compute-thread crashes that faulted the pipeline"
831            )
832            .unwrap()),
833            pipeline_cycle_errors_total: reg!(IntCounter::new(
834                "pipeline_cycle_errors_total",
835                "Fatal cycle errors dropped-and-continued under at-least-once delivery"
836            )
837            .unwrap()),
838            pipeline_restarts_total: reg!(IntCounter::new(
839                "pipeline_restarts_total",
840                "Automatic recover-from-checkpoint restarts after a pipeline fault"
841            )
842            .unwrap()),
843            coordinated_recoveries_total: reg!(IntCounter::new(
844                "coordinated_recoveries_total",
845                "Leader-coordinated global restart-to-epoch rounds applied by this node"
846            )
847            .unwrap()),
848            coordinated_recovery_failures_total: reg!(IntCounter::new(
849                "coordinated_recovery_failures_total",
850                "Leader-coordinated recovery rounds abandoned (self-restore failed or quorum timed out)"
851            )
852            .unwrap()),
853            shuffle_delivery_loss_incidents_total: reg!(IntCounter::new(
854                "shuffle_delivery_loss_incidents_total",
855                "Cross-node shuffle delivery-loss incidents (fences the epoch, forces replay)"
856            )
857            .unwrap()),
858            colocated_routing_fallbacks_total: reg!(IntCounter::new(
859                "colocated_routing_fallbacks_total",
860                "Colocated aggregate input batches repartitioned because rows were owned elsewhere"
861            )
862            .unwrap()),
863        }
864    }
865}
866
867#[cfg(all(test, feature = "cluster"))]
868mod tests {
869    use super::*;
870
871    #[test]
872    fn cluster_subscription_metric_family_has_the_release_names() {
873        let registry = Registry::new();
874        let _metrics = EngineMetrics::new(&registry);
875        let names = registry
876            .gather()
877            .into_iter()
878            .map(|family| family.name().to_owned())
879            .collect::<std::collections::BTreeSet<_>>();
880        for expected in [
881            "cluster_subscription_active_readers",
882            "cluster_subscription_open_total",
883            "cluster_subscription_open_failures_total",
884            "cluster_subscription_frames_committed_total",
885            "cluster_subscription_rows_committed_total",
886            "cluster_subscription_bytes_committed_total",
887            "cluster_subscription_segments_written_total",
888            "cluster_subscription_segment_write_failures_total",
889            "cluster_subscription_manifest_failures_total",
890            "cluster_subscription_integrity_failures_total",
891            "cluster_subscription_stale_writer_rejections_total",
892            "cluster_subscription_sequence_gaps_total",
893            "cluster_subscription_replay_bytes_total",
894            "cluster_subscription_replay_frames_total",
895            "cluster_subscription_replay_pruned_total",
896            "cluster_subscription_gateway_lag_disconnects_total",
897            "cluster_subscription_pending_bytes",
898            "cluster_subscription_retained_bytes",
899            "cluster_subscription_orphan_bytes",
900            "cluster_subscription_checkpoint_prepare_seconds",
901            "cluster_subscription_commit_visibility_seconds",
902            "cluster_subscription_gateway_manifest_refresh_seconds",
903        ] {
904            assert!(names.contains(expected), "missing metric {expected}");
905        }
906    }
907}