Skip to main content

laminar_db/
engine_metrics.rs

1//! Prometheus metrics for the streaming engine.
2
3use prometheus::{
4    Gauge, Histogram, HistogramOpts, HistogramVec, IntCounter, IntCounterVec, IntGauge,
5    IntGaugeVec, Opts, 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
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
214/// Pipeline metrics registered on an explicit prometheus `Registry`.
215///
216/// Constructed once at startup, `Arc`-shared into `PipelineCallback`,
217/// `CheckpointCoordinator`, and `OperatorGraph`.
218pub struct EngineMetrics {
219    /// Committed cluster-subscription metrics without per-stream or per-reader labels.
220    #[cfg(feature = "cluster")]
221    pub cluster_subscription: ClusterSubscriptionMetrics,
222    /// Events ingested from sources.
223    pub events_ingested: IntCounter,
224    /// Events emitted to streams.
225    pub events_emitted: IntCounter,
226    /// Events dropped.
227    pub events_dropped: IntCounter,
228    /// Processing cycles completed.
229    pub cycles: IntCounter,
230    /// Batches processed.
231    pub batches: IntCounter,
232    /// Queries using compiled `PhysicalExpr`.
233    pub queries_compiled: IntCounter,
234    /// Queries using cached logical plan.
235    pub queries_cached_plan: IntCounter,
236    /// Cycles skipped by backpressure.
237    pub cycles_backpressured: IntCounter,
238    /// Materialized view updates.
239    pub mv_updates: IntCounter,
240    /// Approximate MV bytes stored.
241    pub mv_bytes_stored: IntGauge,
242    /// Global pipeline watermark.
243    pub pipeline_watermark: IntGauge,
244    /// Per-source watermark (epoch-ms). Label: `source`.
245    pub source_watermark_ms: IntGaugeVec,
246    /// `1` if a source is idle (excluded from the watermark min), else
247    /// `0`. Label: `source`.
248    pub source_idle: IntGaugeVec,
249    /// Per-stream watermark (epoch-ms). Label: `stream`.
250    pub stream_watermark_ms: IntGaugeVec,
251    /// Per-stream input-port buffered bytes. Label: `stream`.
252    pub input_buf_bytes: IntGaugeVec,
253    /// Operator-reported retained managed-state charge. Labels: `operator`, `phase`.
254    /// `live` is current at sampling; transient `prepared`/`retired` values are the maximum
255    /// observed since the prior sample. Current aggregate values are lower bounds between cold
256    /// reconciliation points and exclude hash buckets, nested/shared payloads, allocator overhead,
257    /// and process RSS.
258    pub managed_state_accounted_bytes: IntGaugeVec,
259    /// Per-stream rows shed by the `ShedOldest` policy. Label: `stream`.
260    pub shed_records_total: IntCounterVec,
261    /// Completed checkpoints.
262    pub checkpoints_completed: IntCounter,
263    /// Failed checkpoints.
264    pub checkpoints_failed: IntCounter,
265    /// Current checkpoint epoch.
266    pub checkpoint_epoch: IntGauge,
267    /// Last checkpoint size in bytes.
268    pub checkpoint_size_bytes: IntGauge,
269    /// Sink write errors.
270    pub sink_write_failures: IntCounter,
271    /// Sink write timeouts.
272    pub sink_write_timeouts: IntCounter,
273    /// Sink task channel closed.
274    pub sink_task_channel_closed: IntCounter,
275    /// Rows dropped because the sink's WHERE filter failed to compile to
276    /// a `PhysicalExpr` (fail-closed). Label: `sink`.
277    pub sink_filter_rejected_rows: IntCounterVec,
278    /// Window assignments dropped past `allowed_lateness`. A hopping-window row can contribute
279    /// more than one assignment.
280    pub window_late_dropped: IntCounter,
281    /// Source rows dropped because the event-time column was null.
282    pub events_null_timestamp: IntCounter,
283    /// Rows currently buffered by temporal-filter operators.
284    pub temporal_filter_buffered: IntGauge,
285    /// Z-set inserts (+1) emitted by temporal-filter operators.
286    pub temporal_filter_inserts: IntCounter,
287    /// Z-set retractions (-1) emitted by temporal-filter operators.
288    pub temporal_filter_retracts: IntCounter,
289    /// Late / born-expired / beyond-horizon rows dropped un-emitted.
290    pub temporal_filter_dropped: IntCounter,
291    /// Per-cycle processing duration.
292    pub cycle_duration: Histogram,
293    /// SQL operator-graph execution within successful normal cycles.
294    pub cycle_execute_duration: Histogram,
295    /// Materialized-view and subscription-stream publication within successful normal cycles.
296    pub cycle_output_store_duration: Histogram,
297    /// Sink command admission within successful normal cycles.
298    pub cycle_sink_enqueue_duration: Histogram,
299    /// Per-operator processing duration. Labels: `operator`, `mode` (`normal` or
300    /// `checkpoint_drain`).
301    pub operator_process_duration: HistogramVec,
302    /// Checkpoint cycle duration.
303    pub checkpoint_duration: Histogram,
304    /// Synchronous mutable checkpoint-state capture duration.
305    pub checkpoint_state_capture_duration: Histogram,
306    /// Pipeline stall per barrier: sink write fence, shuffle alignment, state capture, durable-tail
307    /// handoff, and the Aligned resume gate. Every durable tail remains supervised in the
308    /// background; committable sink writes wait on its exact successor-epoch gate.
309    pub checkpoint_pipeline_stall_duration: Histogram,
310    /// Local barrier work while the pipeline is paused: sink fencing, shuffle alignment, state
311    /// capture, and construction of the immutable durable-tail handoff.
312    pub checkpoint_barrier_local_duration: Histogram,
313    /// Cluster-shuffle pause after local capture while waiting for the global Aligned release.
314    /// Embedded and single-node runtimes do not observe this metric.
315    pub checkpoint_aligned_resume_wait: Histogram,
316    /// Sink pre-commit round-trip (2PC phase 1).
317    pub sink_precommit_duration: Histogram,
318    /// On-demand lookup cache hits (served without a source fetch). Label: `table`.
319    pub lookup_cache_hits: IntCounterVec,
320    /// On-demand lookup cache misses (not in cache). Label: `table`.
321    pub lookup_cache_misses: IntCounterVec,
322    /// On-demand lookup source fetch errors/timeouts. Label: `table`.
323    pub lookup_source_errors: IntCounterVec,
324    /// On-demand lookup rows awaiting a source fetch. Label: `table`.
325    pub lookup_in_flight_rows: IntGaugeVec,
326    /// Vnodes owned per failure domain (cluster mode). Label: `domain`.
327    pub placement_vnodes_per_domain: IntGaugeVec,
328    /// Largest single domain's share of all vnodes (`[0, 1]`) — the blast radius.
329    pub placement_blast_radius_ratio: Gauge,
330    /// Unexpected compute-thread exits (operator panics) that faulted the pipeline.
331    pub pipeline_faults_total: IntCounter,
332    /// Fatal cycle errors dropped-and-continued under at-least-once delivery.
333    pub pipeline_cycle_errors_total: IntCounter,
334    /// Automatic recover-from-checkpoint restarts triggered by the fault supervisor.
335    pub pipeline_restarts_total: IntCounter,
336    /// Leader-coordinated global restart-to-epoch rounds this node applied (cluster mode).
337    pub coordinated_recoveries_total: IntCounter,
338    /// Leader-coordinated recovery rounds abandoned (self-restore failed or restore quorum timed out).
339    pub coordinated_recovery_failures_total: IntCounter,
340    /// Shuffle delivery-loss incidents; each one fences an epoch and forces replay.
341    pub shuffle_delivery_loss_incidents_total: IntCounter,
342}
343
344impl EngineMetrics {
345    /// Register all engine metrics on the given registry. Startup only.
346    ///
347    /// # Panics
348    ///
349    /// Panics if metric registration fails (duplicate names).
350    #[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            // Labels are catalog-bound, so cardinality is finite.
402            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            // Operator names are catalog-bound and phase is one of live/prepared/retired.
426            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 names are catalog-bound and mode has exactly two values.
555            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: serialization_timeout=120s, so max bucket must cover that.
565            // 0.01 * 2^14 = 163.84s.
566            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            // Stall target is sub-second; the resume gate is bounded at
580            // 30s. 0.001 * 2^15 = 32.77s.
581            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            // The one attempt deadline defaults to 120s. 0.005 * 2^15 = 163.84s.
606            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            // Labels are bound to the registered lookup tables, so cardinality is finite.
612            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(&registry);
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}