Skip to main content

laminar_db/
engine_metrics.rs

1//! Prometheus metrics for the streaming engine.
2
3use prometheus::{
4    Gauge, Histogram, HistogramOpts, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Opts,
5    Registry,
6};
7
8/// Pipeline metrics registered on an explicit prometheus `Registry`.
9///
10/// Constructed once at startup, `Arc`-shared into `PipelineCallback`,
11/// `CheckpointCoordinator`, and `OperatorGraph`.
12pub struct EngineMetrics {
13    /// Events ingested from sources.
14    pub events_ingested: IntCounter,
15    /// Events emitted to streams.
16    pub events_emitted: IntCounter,
17    /// Events dropped.
18    pub events_dropped: IntCounter,
19    /// Processing cycles completed.
20    pub cycles: IntCounter,
21    /// Batches processed.
22    pub batches: IntCounter,
23    /// Queries using compiled `PhysicalExpr`.
24    pub queries_compiled: IntCounter,
25    /// Queries using cached logical plan.
26    pub queries_cached_plan: IntCounter,
27    /// Cycles skipped by backpressure.
28    pub cycles_backpressured: IntCounter,
29    /// Materialized view updates.
30    pub mv_updates: IntCounter,
31    /// Approximate MV bytes stored.
32    pub mv_bytes_stored: IntGauge,
33    /// Global pipeline watermark.
34    pub pipeline_watermark: IntGauge,
35    /// Per-source watermark (epoch-ms). Label: `source`.
36    pub source_watermark_ms: IntGaugeVec,
37    /// `1` if a source is idle (excluded from the watermark min), else
38    /// `0`. Label: `source`.
39    pub source_idle: IntGaugeVec,
40    /// Per-stream watermark (epoch-ms). Label: `stream`.
41    pub stream_watermark_ms: IntGaugeVec,
42    /// Per-stream input-port buffered bytes. Label: `stream`.
43    pub input_buf_bytes: IntGaugeVec,
44    /// Per-stream rows shed by the `ShedOldest` policy. Label: `stream`.
45    pub shed_records_total: IntCounterVec,
46    /// Completed checkpoints.
47    pub checkpoints_completed: IntCounter,
48    /// Failed checkpoints.
49    pub checkpoints_failed: IntCounter,
50    /// Current checkpoint epoch.
51    pub checkpoint_epoch: IntGauge,
52    /// Last checkpoint size in bytes.
53    pub checkpoint_size_bytes: IntGauge,
54    /// Sink write errors.
55    pub sink_write_failures: IntCounter,
56    /// Sink write timeouts.
57    pub sink_write_timeouts: IntCounter,
58    /// Sink task channel closed.
59    pub sink_task_channel_closed: IntCounter,
60    /// Rows dropped because the sink's WHERE filter failed to compile to
61    /// a `PhysicalExpr` (fail-closed). Label: `sink`.
62    pub sink_filter_rejected_rows: IntCounterVec,
63    /// Rows dropped at operator level past `allowed_lateness` (distinct
64    /// from `events_dropped`, which is source-side).
65    pub window_late_dropped: IntCounter,
66    /// Source rows dropped because the event-time column was null.
67    pub events_null_timestamp: IntCounter,
68    /// Rows currently buffered by temporal-filter operators.
69    pub temporal_filter_buffered: IntGauge,
70    /// Z-set inserts (+1) emitted by temporal-filter operators.
71    pub temporal_filter_inserts: IntCounter,
72    /// Z-set retractions (-1) emitted by temporal-filter operators.
73    pub temporal_filter_retracts: IntCounter,
74    /// Late / born-expired / beyond-horizon rows dropped un-emitted.
75    pub temporal_filter_dropped: IntCounter,
76    /// Per-cycle processing duration.
77    pub cycle_duration: Histogram,
78    /// Checkpoint cycle duration.
79    pub checkpoint_duration: Histogram,
80    /// Pipeline stall per barrier: sink write fence, shuffle alignment, state capture, and the
81    /// Aligned resume gate. Exactly-once mode also includes its inline durable tail; other modes
82    /// resume at Aligned while that tail remains supervised in the background.
83    pub checkpoint_pipeline_stall_duration: Histogram,
84    /// Local barrier work while the pipeline is paused: sink fencing, shuffle alignment, state
85    /// capture, and construction of the immutable durable-tail handoff.
86    pub checkpoint_barrier_local_duration: Histogram,
87    /// Cluster-shuffle pause after local capture while waiting for the global Aligned release.
88    /// Embedded and single-node runtimes do not observe this metric.
89    pub checkpoint_aligned_resume_wait: Histogram,
90    /// Time the leader's restorable gate spends polling for vnode
91    /// partials (failed gates that burn the timeout are observed too).
92    /// When this dominates restorable latency at production cadence,
93    /// the push-driven upload-completion-ack follow-up is worth
94    /// building.
95    pub checkpoint_restorable_gate_wait: Histogram,
96    /// Vnode partials written as references to an unchanged base
97    /// instead of re-uploading state.
98    pub checkpoint_unchanged_vnodes: IntCounter,
99    /// Sealed-but-not-yet-externally-committed epochs for coordinated sinks
100    /// (designated-committer lag). Rising = the committer can't keep up.
101    pub coordinated_committer_lag_epochs: IntGauge,
102    /// Sink pre-commit round-trip (2PC phase 1).
103    pub sink_precommit_duration: Histogram,
104    /// On-demand lookup cache hits (served without a source fetch). Label: `table`.
105    pub lookup_cache_hits: IntCounterVec,
106    /// On-demand lookup cache misses (not in cache). Label: `table`.
107    pub lookup_cache_misses: IntCounterVec,
108    /// On-demand lookup source fetch errors/timeouts. Label: `table`.
109    pub lookup_source_errors: IntCounterVec,
110    /// On-demand lookup rows awaiting a source fetch. Label: `table`.
111    pub lookup_in_flight_rows: IntGaugeVec,
112    /// Vnodes owned per failure domain (cluster mode). Label: `domain`.
113    pub placement_vnodes_per_domain: IntGaugeVec,
114    /// Largest single domain's share of all vnodes (`[0, 1]`) — the blast radius.
115    pub placement_blast_radius_ratio: Gauge,
116    /// Unexpected compute-thread exits (operator panics) that faulted the pipeline.
117    pub pipeline_faults_total: IntCounter,
118    /// Fatal cycle errors dropped-and-continued under at-least-once delivery.
119    pub pipeline_cycle_errors_total: IntCounter,
120    /// Automatic recover-from-checkpoint restarts triggered by the fault supervisor.
121    pub pipeline_restarts_total: IntCounter,
122    /// Leader-coordinated global restart-to-epoch rounds this node applied (cluster mode).
123    pub coordinated_recoveries_total: IntCounter,
124    /// Leader-coordinated recovery rounds abandoned (self-restore failed or restore quorum timed out).
125    pub coordinated_recovery_failures_total: IntCounter,
126    /// Shuffle delivery-loss incidents; each one fences an epoch and forces replay.
127    pub shuffle_delivery_loss_incidents_total: IntCounter,
128}
129
130impl EngineMetrics {
131    /// Register all engine metrics on the given registry. Startup only.
132    ///
133    /// # Panics
134    ///
135    /// Panics if metric registration fails (duplicate names).
136    #[must_use]
137    #[allow(clippy::too_many_lines)]
138    pub fn new(registry: &Registry) -> Self {
139        macro_rules! reg {
140            ($m:expr) => {{
141                let m = $m;
142                registry.register(Box::new(m.clone())).unwrap();
143                m
144            }};
145        }
146
147        Self {
148            events_ingested: reg!(IntCounter::new(
149                "events_ingested_total",
150                "Events ingested from sources"
151            )
152            .unwrap()),
153            events_emitted: reg!(IntCounter::new(
154                "events_emitted_total",
155                "Events emitted to streams"
156            )
157            .unwrap()),
158            events_dropped: reg!(IntCounter::new("events_dropped_total", "Events dropped").unwrap()),
159            cycles: reg!(IntCounter::new("cycles_total", "Processing cycles completed").unwrap()),
160            batches: reg!(IntCounter::new("batches_total", "Batches processed").unwrap()),
161            queries_compiled: reg!(IntCounter::new(
162                "queries_compiled_total",
163                "Queries using compiled PhysicalExpr"
164            )
165            .unwrap()),
166            queries_cached_plan: reg!(IntCounter::new(
167                "queries_cached_plan_total",
168                "Queries using cached logical plan"
169            )
170            .unwrap()),
171            cycles_backpressured: reg!(IntCounter::new(
172                "cycles_backpressured_total",
173                "Cycles skipped by backpressure"
174            )
175            .unwrap()),
176            mv_updates: reg!(
177                IntCounter::new("mv_updates_total", "Materialized view updates").unwrap()
178            ),
179            mv_bytes_stored: reg!(
180                IntGauge::new("mv_bytes_stored", "Approximate MV bytes stored").unwrap()
181            ),
182            pipeline_watermark: reg!(IntGauge::new(
183                "pipeline_watermark",
184                "Global pipeline watermark"
185            )
186            .unwrap()),
187            // Labels are catalog-bound, so cardinality is finite.
188            source_watermark_ms: reg!(IntGaugeVec::new(
189                Opts::new("source_watermark_ms", "Per-source watermark (epoch-ms)"),
190                &["source"],
191            )
192            .unwrap()),
193            source_idle: reg!(IntGaugeVec::new(
194                Opts::new(
195                    "source_idle",
196                    "1 if source idle (excluded from watermark min)"
197                ),
198                &["source"],
199            )
200            .unwrap()),
201            stream_watermark_ms: reg!(IntGaugeVec::new(
202                Opts::new("stream_watermark_ms", "Per-stream watermark (epoch-ms)"),
203                &["stream"],
204            )
205            .unwrap()),
206            input_buf_bytes: reg!(IntGaugeVec::new(
207                Opts::new("input_buf_bytes", "Per-stream input buffer bytes"),
208                &["stream"],
209            )
210            .unwrap()),
211            shed_records_total: reg!(IntCounterVec::new(
212                Opts::new("shed_records_total", "Rows shed by ShedOldest policy"),
213                &["stream"],
214            )
215            .unwrap()),
216            checkpoints_completed: reg!(IntCounter::new(
217                "checkpoints_completed_total",
218                "Completed checkpoints"
219            )
220            .unwrap()),
221            checkpoints_failed: reg!(IntCounter::new(
222                "checkpoints_failed_total",
223                "Failed checkpoints"
224            )
225            .unwrap()),
226            checkpoint_epoch: reg!(
227                IntGauge::new("checkpoint_epoch", "Current checkpoint epoch").unwrap()
228            ),
229            checkpoint_size_bytes: reg!(IntGauge::new(
230                "checkpoint_size_bytes",
231                "Last checkpoint size"
232            )
233            .unwrap()),
234            sink_write_failures: reg!(IntCounter::new(
235                "sink_write_failures_total",
236                "Sink write errors"
237            )
238            .unwrap()),
239            sink_write_timeouts: reg!(IntCounter::new(
240                "sink_write_timeouts_total",
241                "Sink write timeouts"
242            )
243            .unwrap()),
244            sink_task_channel_closed: reg!(IntCounter::new(
245                "sink_task_channel_closed_total",
246                "Sink task channel closed"
247            )
248            .unwrap()),
249            sink_filter_rejected_rows: reg!(IntCounterVec::new(
250                Opts::new(
251                    "sink_filter_rejected_rows_total",
252                    "Rows dropped because the sink filter failed to compile",
253                ),
254                &["sink"],
255            )
256            .unwrap()),
257            window_late_dropped: reg!(IntCounter::new(
258                "window_late_dropped_total",
259                "Rows dropped by window operators past allowed_lateness"
260            )
261            .unwrap()),
262            events_null_timestamp: reg!(IntCounter::new(
263                "events_null_timestamp_total",
264                "Source rows dropped because the event-time column was null"
265            )
266            .unwrap()),
267            temporal_filter_buffered: reg!(IntGauge::new(
268                "temporal_filter_buffered",
269                "Rows buffered by retracting temporal-filter operators"
270            )
271            .unwrap()),
272            temporal_filter_inserts: reg!(IntCounter::new(
273                "temporal_filter_inserts_total",
274                "Z-set inserts emitted by temporal-filter operators"
275            )
276            .unwrap()),
277            temporal_filter_retracts: reg!(IntCounter::new(
278                "temporal_filter_retracts_total",
279                "Z-set retractions emitted by temporal-filter operators"
280            )
281            .unwrap()),
282            temporal_filter_dropped: reg!(IntCounter::new(
283                "temporal_filter_dropped_total",
284                "Rows dropped un-emitted by temporal-filter operators"
285            )
286            .unwrap()),
287            cycle_duration: reg!(Histogram::with_opts(
288                HistogramOpts::new("cycle_duration_seconds", "Per-cycle processing duration")
289                    .buckets(vec![
290                        1e-7, 5e-7, 1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 5e-3, 1e-2, 5e-2,
291                        1e-1, 5e-1, 1.0,
292                    ]),
293            )
294            .unwrap()),
295            // Checkpoint: serialization_timeout=120s, so max bucket must cover that.
296            // 0.01 * 2^14 = 163.84s.
297            checkpoint_duration: reg!(Histogram::with_opts(
298                HistogramOpts::new("checkpoint_duration_seconds", "Checkpoint cycle duration")
299                    .buckets(prometheus::exponential_buckets(0.01, 2.0, 15).unwrap()),
300            )
301            .unwrap()),
302            // Stall target is sub-second; the resume gate is bounded at
303            // 30s. 0.001 * 2^15 = 32.77s.
304            checkpoint_pipeline_stall_duration: reg!(Histogram::with_opts(
305                HistogramOpts::new(
306                    "checkpoint_pipeline_stall_duration_seconds",
307                    "Pipeline stall per checkpoint barrier (align + capture + resume gate)",
308                )
309                .buckets(prometheus::exponential_buckets(0.001, 2.0, 16).unwrap()),
310            )
311            .unwrap()),
312            checkpoint_barrier_local_duration: reg!(Histogram::with_opts(
313                HistogramOpts::new(
314                    "checkpoint_barrier_local_duration_seconds",
315                    "Local paused barrier work (sink fence + shuffle align + state capture + tail handoff)",
316                )
317                .buckets(prometheus::exponential_buckets(0.001, 2.0, 16).unwrap()),
318            )
319            .unwrap()),
320            checkpoint_aligned_resume_wait: reg!(Histogram::with_opts(
321                HistogramOpts::new(
322                    "checkpoint_aligned_resume_wait_seconds",
323                    "Cluster-shuffle pause waiting for the global Aligned release",
324                )
325                .buckets(prometheus::exponential_buckets(0.001, 2.0, 16).unwrap()),
326            )
327            .unwrap()),
328            // Gate timeout default 10s. 0.001 * 2^14 = 16.38s.
329            checkpoint_restorable_gate_wait: reg!(Histogram::with_opts(
330                HistogramOpts::new(
331                    "checkpoint_restorable_gate_wait_seconds",
332                    "Restorable-gate poll wait per epoch (vnode-partial presence)",
333                )
334                .buckets(prometheus::exponential_buckets(0.001, 2.0, 15).unwrap()),
335            )
336            .unwrap()),
337            coordinated_committer_lag_epochs: reg!(IntGauge::new(
338                "coordinated_committer_lag_epochs",
339                "Sealed epochs not yet externally committed by the designated committer",
340            )
341            .unwrap()),
342            checkpoint_unchanged_vnodes: reg!(IntCounter::new(
343                "checkpoint_unchanged_vnodes_total",
344                "Vnode partials written as unchanged-base references"
345            )
346            .unwrap()),
347            // The one attempt deadline defaults to 120s. 0.005 * 2^15 = 163.84s.
348            sink_precommit_duration: reg!(Histogram::with_opts(
349                HistogramOpts::new("sink_precommit_duration_seconds", "Sink pre-commit latency")
350                    .buckets(prometheus::exponential_buckets(0.005, 2.0, 16).unwrap()),
351            )
352            .unwrap()),
353            // Labels are bound to the registered lookup tables, so cardinality is finite.
354            lookup_cache_hits: reg!(IntCounterVec::new(
355                Opts::new("lookup_cache_hits_total", "On-demand lookup cache hits"),
356                &["table"],
357            )
358            .unwrap()),
359            lookup_cache_misses: reg!(IntCounterVec::new(
360                Opts::new("lookup_cache_misses_total", "On-demand lookup cache misses"),
361                &["table"],
362            )
363            .unwrap()),
364            lookup_source_errors: reg!(IntCounterVec::new(
365                Opts::new(
366                    "lookup_source_errors_total",
367                    "On-demand lookup source fetch errors"
368                ),
369                &["table"],
370            )
371            .unwrap()),
372            lookup_in_flight_rows: reg!(IntGaugeVec::new(
373                Opts::new(
374                    "lookup_in_flight_rows",
375                    "On-demand lookup rows awaiting a source fetch"
376                ),
377                &["table"],
378            )
379            .unwrap()),
380            placement_vnodes_per_domain: reg!(IntGaugeVec::new(
381                Opts::new(
382                    "placement_vnodes_per_domain",
383                    "Vnodes owned per failure domain (cluster mode)"
384                ),
385                &["domain"],
386            )
387            .unwrap()),
388            placement_blast_radius_ratio: reg!(Gauge::new(
389                "placement_blast_radius_ratio",
390                "Largest single domain's share of all vnodes (0-1); state that goes Restoring if it fails"
391            )
392            .unwrap()),
393            pipeline_faults_total: reg!(IntCounter::new(
394                "pipeline_faults_total",
395                "Compute-thread crashes that faulted the pipeline"
396            )
397            .unwrap()),
398            pipeline_cycle_errors_total: reg!(IntCounter::new(
399                "pipeline_cycle_errors_total",
400                "Fatal cycle errors dropped-and-continued under at-least-once delivery"
401            )
402            .unwrap()),
403            pipeline_restarts_total: reg!(IntCounter::new(
404                "pipeline_restarts_total",
405                "Automatic recover-from-checkpoint restarts after a pipeline fault"
406            )
407            .unwrap()),
408            coordinated_recoveries_total: reg!(IntCounter::new(
409                "coordinated_recoveries_total",
410                "Leader-coordinated global restart-to-epoch rounds applied by this node"
411            )
412            .unwrap()),
413            coordinated_recovery_failures_total: reg!(IntCounter::new(
414                "coordinated_recovery_failures_total",
415                "Leader-coordinated recovery rounds abandoned (self-restore failed or quorum timed out)"
416            )
417            .unwrap()),
418            shuffle_delivery_loss_incidents_total: reg!(IntCounter::new(
419                "shuffle_delivery_loss_incidents_total",
420                "Cross-node shuffle delivery-loss incidents (fences the epoch, forces replay)"
421            )
422            .unwrap()),
423        }
424    }
425}