Skip to main content

mz_persist_client/internal/
metrics.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Prometheus monitoring metrics.
11
12use async_stream::stream;
13use mz_persist_types::stats::PartStatsMetrics;
14use std::collections::BTreeMap;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::{Arc, Mutex, Weak};
17use std::time::{Duration, Instant};
18use tokio::sync::{OnceCell, OwnedSemaphorePermit, Semaphore};
19
20use async_trait::async_trait;
21use bytes::Bytes;
22use futures_util::StreamExt;
23use mz_dyncfg::ConfigSet;
24use mz_ore::bytes::SegmentedBytes;
25use mz_ore::cast::{CastFrom, CastLossy};
26use mz_ore::instrument;
27use mz_ore::metric;
28use mz_ore::metrics::aggregation::{AggregatedFamilies, AggregatedFamily, AggregationKey};
29use mz_ore::metrics::{
30    ComputedGauge, ComputedUIntGauge, Counter, DeleteOnDropCounter, DeleteOnDropGauge, IntCounter,
31    MakeCollector, MetricsRegistry, PrometheusOpts, UIntGauge, UIntGaugeVec, raw,
32};
33use mz_ore::stats::histogram_seconds_buckets;
34use mz_persist::location::{
35    Blob, BlobMetadata, CaSResult, Consensus, ExternalError, ResultStream, SeqNo, VersionedData,
36};
37use mz_persist::metrics::{BlobHedgeMetrics, ColumnarMetrics, S3BlobMetrics};
38use mz_persist::retry::RetryStream;
39use mz_persist_types::Codec64;
40use mz_postgres_client::metrics::PostgresClientMetrics;
41use prometheus::core::{AtomicI64, AtomicU64, Collector, Desc, GenericGauge};
42use prometheus::proto::MetricFamily;
43use prometheus::{CounterVec, Gauge, GaugeVec, Histogram, HistogramVec, IntCounterVec};
44use regex::Regex;
45use timely::progress::Antichain;
46use tokio_metrics::TaskMonitor;
47use tracing::{Instrument, debug, info, info_span, warn};
48
49use crate::cfg::{
50    PER_SHARD_METRICS_ENABLE_REGEX, SHARD_INFO_METRIC, SHARD_METRICS, ShardMetricsExport,
51};
52use crate::fetch::{FETCH_SEMAPHORE_COST_ADJUSTMENT, FETCH_SEMAPHORE_PERMIT_ADJUSTMENT};
53use crate::internal::paths::BlobKey;
54use crate::{PersistConfig, ShardId};
55
56/// Prometheus monitoring metrics.
57///
58/// Intentionally not Clone because we expect this to be passed around in an
59/// Arc.
60pub struct Metrics {
61    _vecs: MetricsVecs,
62    _uptime: ComputedGauge,
63
64    /// Metrics for [Blob] usage.
65    pub blob: BlobMetrics,
66    /// Metrics for [Consensus] usage.
67    pub consensus: ConsensusMetrics,
68    /// Metrics of command evaluation.
69    pub cmds: CmdsMetrics,
70    /// Metrics for each retry loop.
71    pub retries: RetriesMetrics,
72    /// Metrics for batches written directly on behalf of a user (BatchBuilder
73    /// or one of the sugar methods that use it).
74    pub user: BatchWriteMetrics,
75    /// Metrics for reading batch parts
76    pub read: BatchPartReadMetrics,
77    /// Metrics for compaction.
78    pub compaction: CompactionMetrics,
79    /// Metrics for garbage collection.
80    pub gc: GcMetrics,
81    /// Metrics for leasing and automatic lease expiry.
82    pub lease: LeaseMetrics,
83    /// Metrics for various encodings and decodings.
84    pub codecs: CodecsMetrics,
85    /// Metrics for (incremental) state updates and fetches.
86    pub state: StateMetrics,
87    /// Metrics for various per-shard measurements.
88    pub shards: ShardsMetrics,
89    /// Metrics for auditing persist usage
90    pub audit: UsageAuditMetrics,
91    /// Metrics for locking.
92    pub locks: LocksMetrics,
93    /// Metrics for StateWatch.
94    pub watch: WatchMetrics,
95    /// Metrics for PubSub client.
96    pub pubsub_client: PubSubClientMetrics,
97    /// Metrics for mfp/filter pushdown.
98    pub pushdown: PushdownMetrics,
99    /// Metrics for consolidation.
100    pub consolidation: ConsolidationMetrics,
101    /// Metrics for blob caching.
102    pub blob_cache_mem: BlobMemCache,
103    /// Metrics for tokio tasks.
104    pub tasks: TasksMetrics,
105    /// Metrics for columnar data encoding and decoding.
106    pub columnar: ColumnarMetrics,
107    /// Metrics for schemas and the schema registry.
108    pub schema: SchemaMetrics,
109    /// Metrics for inline writes.
110    pub inline: InlineMetrics,
111    /// Semaphore to limit memory/disk use by fetches.
112    pub(crate) semaphore: SemaphoreMetrics,
113
114    /// Metrics for the persist sink.
115    pub sink: SinkMetrics,
116    /// Metrics for the persist_source backpressure operator.
117    pub backpressure: BackpressureMetrics,
118
119    /// Metrics for S3-backed blob implementation
120    pub s3_blob: S3BlobMetrics,
121    /// Metrics for hedged blob gets
122    pub blob_hedge: BlobHedgeMetrics,
123    /// Metrics for Postgres-backed consensus implementation
124    pub postgres_consensus: PostgresClientMetrics,
125
126    #[allow(dead_code)]
127    pub(crate) registry: MetricsRegistry,
128}
129
130impl std::fmt::Debug for Metrics {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        f.debug_struct("Metrics").finish_non_exhaustive()
133    }
134}
135
136impl Metrics {
137    /// Returns a new [Metrics] instance connected to the given registry.
138    pub fn new(cfg: &PersistConfig, registry: &MetricsRegistry) -> Self {
139        let vecs = MetricsVecs::new(registry);
140        let start = Instant::now();
141        let uptime = registry.register_computed_gauge(
142            metric!(
143                name: "mz_persist_metadata_seconds",
144                help: "server uptime, labels are build metadata",
145                const_labels: {
146                    "version" => cfg.build_version,
147                    "build_type" => if cfg!(release) { "release" } else { "debug" }
148                },
149            ),
150            move || start.elapsed().as_secs_f64(),
151        );
152        let s3_blob = S3BlobMetrics::new(registry);
153        let columnar = ColumnarMetrics::new(registry);
154        let mut per_shard_filter = PerShardMetricsFilter {
155            configs: Arc::clone(&cfg.configs),
156            mode: (String::new(), ShardMetricsExport::default()),
157            invalid_mode: registry.register(metric!(
158                name: "mz_persist_shard_metrics_mode_invalid",
159                help: "count of metrics scrapes that found persist_shard_metrics unrecognized and so exported the default",
160            )),
161            allow: (String::new(), None),
162            invalid_regex: registry.register(metric!(
163                name: "mz_persist_per_shard_metrics_regex_invalid",
164                help: "count of metrics scrapes that found persist_per_shard_metrics_enable_regex invalid and so kept no per-shard series",
165            )),
166        };
167        registry.register_postprocessor(move |families| per_shard_filter.filter(families));
168        Metrics {
169            blob: vecs.blob_metrics(),
170            consensus: vecs.consensus_metrics(),
171            cmds: vecs.cmds_metrics(registry),
172            retries: vecs.retries_metrics(),
173            codecs: vecs.codecs_metrics(),
174            user: BatchWriteMetrics::new(registry, "user"),
175            read: vecs.batch_part_read_metrics(),
176            compaction: CompactionMetrics::new(registry),
177            gc: GcMetrics::new(registry),
178            lease: LeaseMetrics::new(registry),
179            state: StateMetrics::new(registry),
180            shards: ShardsMetrics::new(registry, Arc::clone(&cfg.configs)),
181            audit: UsageAuditMetrics::new(registry),
182            locks: vecs.locks_metrics(),
183            watch: WatchMetrics::new(registry),
184            pubsub_client: PubSubClientMetrics::new(registry),
185            pushdown: PushdownMetrics::new(registry),
186            consolidation: ConsolidationMetrics::new(registry),
187            blob_cache_mem: BlobMemCache::new(registry),
188            tasks: TasksMetrics::new(registry),
189            columnar,
190            schema: SchemaMetrics::new(registry),
191            inline: InlineMetrics::new(registry),
192            semaphore: SemaphoreMetrics::new(cfg.clone(), registry.clone()),
193            sink: SinkMetrics::new(registry),
194            backpressure: BackpressureMetrics::new(registry),
195            s3_blob,
196            blob_hedge: BlobHedgeMetrics::new(registry),
197            postgres_consensus: PostgresClientMetrics::new(registry, "mz_persist"),
198            _vecs: vecs,
199            _uptime: uptime,
200            registry: registry.clone(),
201        }
202    }
203
204    /// Returns the current lifetime write amplification reflected in these
205    /// metrics.
206    ///
207    /// Only exposed for tests, persistcli, and benchmarks.
208    pub fn write_amplification(&self) -> f64 {
209        // This intentionally uses "bytes" for total and "goodbytes" for user so
210        // that the overhead of our blob format is included.
211        let total_written = self.blob.set.bytes.get();
212        let user_written = self.user.goodbytes.get();
213        #[allow(clippy::as_conversions)]
214        {
215            total_written as f64 / user_written as f64
216        }
217    }
218}
219
220#[derive(Debug)]
221struct MetricsVecs {
222    cmd_started: IntCounterVec,
223    cmd_cas_mismatch: IntCounterVec,
224    cmd_succeeded: IntCounterVec,
225    cmd_failed: IntCounterVec,
226    cmd_seconds: CounterVec,
227
228    external_op_started: IntCounterVec,
229    external_op_succeeded: IntCounterVec,
230    external_op_failed: IntCounterVec,
231    external_op_bytes: IntCounterVec,
232    external_op_seconds: CounterVec,
233    external_consensus_truncated_count: IntCounter,
234    external_blob_delete_noop_count: IntCounter,
235    external_blob_sizes: Histogram,
236    external_rtt_latency: GaugeVec,
237    external_op_latency: HistogramVec,
238
239    retry_started: IntCounterVec,
240    retry_finished: IntCounterVec,
241    retry_retries: IntCounterVec,
242    retry_sleep_seconds: CounterVec,
243
244    encode_count: IntCounterVec,
245    encode_seconds: CounterVec,
246    decode_count: IntCounterVec,
247    decode_seconds: CounterVec,
248
249    read_part_bytes: IntCounterVec,
250    read_part_goodbytes: IntCounterVec,
251    read_part_count: IntCounterVec,
252    read_part_seconds: CounterVec,
253    read_ts_rewrite: IntCounterVec,
254
255    lock_acquire_count: IntCounterVec,
256    lock_blocking_acquire_count: IntCounterVec,
257    lock_blocking_seconds: CounterVec,
258
259    /// A minimal set of metrics imported into honeycomb for alerting.
260    alerts_metrics: Arc<AlertsMetrics>,
261}
262
263impl MetricsVecs {
264    fn new(registry: &MetricsRegistry) -> Self {
265        MetricsVecs {
266            cmd_started: registry.register(metric!(
267                name: "mz_persist_cmd_started_count",
268                help: "count of commands started",
269                var_labels: ["cmd"],
270            )),
271            cmd_cas_mismatch: registry.register(metric!(
272                name: "mz_persist_cmd_cas_mismatch_count",
273                help: "count of command retries from CaS mismatch",
274                var_labels: ["cmd"],
275            )),
276            cmd_succeeded: registry.register(metric!(
277                name: "mz_persist_cmd_succeeded_count",
278                help: "count of commands succeeded",
279                var_labels: ["cmd"],
280            )),
281            cmd_failed: registry.register(metric!(
282                name: "mz_persist_cmd_failed_count",
283                help: "count of commands failed",
284                var_labels: ["cmd"],
285            )),
286            cmd_seconds: registry.register(metric!(
287                name: "mz_persist_cmd_seconds",
288                help: "time spent applying commands",
289                var_labels: ["cmd"],
290            )),
291
292            external_op_started: registry.register(metric!(
293                name: "mz_persist_external_started_count",
294                help: "count of external service calls started",
295                var_labels: ["op"],
296            )),
297            external_op_succeeded: registry.register(metric!(
298                name: "mz_persist_external_succeeded_count",
299                help: "count of external service calls succeeded",
300                var_labels: ["op"],
301            )),
302            external_op_failed: registry.register(metric!(
303                name: "mz_persist_external_failed_count",
304                help: "count of external service calls failed",
305                var_labels: ["op"],
306            )),
307            external_op_bytes: registry.register(metric!(
308                name: "mz_persist_external_bytes_count",
309                help: "total size represented by external service calls",
310                var_labels: ["op"],
311            )),
312            external_op_seconds: registry.register(metric!(
313                name: "mz_persist_external_seconds",
314                help: "time spent in external service calls",
315                var_labels: ["op"],
316            )),
317            external_consensus_truncated_count: registry.register(metric!(
318                name: "mz_persist_external_consensus_truncated_count",
319                help: "count of versions deleted by consensus truncate calls",
320            )),
321            external_blob_delete_noop_count: registry.register(metric!(
322                name: "mz_persist_external_blob_delete_noop_count",
323                help: "count of blob delete calls that deleted a non-existent key",
324            )),
325            external_blob_sizes: registry.register(metric!(
326                name: "mz_persist_external_blob_sizes",
327                help: "histogram of blob sizes at put time",
328                buckets: mz_ore::stats::HISTOGRAM_BYTE_BUCKETS.to_vec(),
329            )),
330            external_rtt_latency: registry.register(metric!(
331                name: "mz_persist_external_rtt_latency",
332                help: "roundtrip-time to external service as seen by this process",
333                var_labels: ["external"],
334            )),
335            external_op_latency: registry.register(metric!(
336                name: "mz_persist_external_op_latency",
337                help: "rountrip latency observed by individual performance-critical operations",
338                var_labels: ["op"],
339                // NB: If we end up overrunning metrics quotas, we could plausibly cut this
340                // down by switching to a factor of 4 between buckets (vs. the standard 2).
341                buckets: histogram_seconds_buckets(0.000_500, 32.0),
342            )),
343
344            retry_started: registry.register(metric!(
345                name: "mz_persist_retry_started_count",
346                help: "count of retry loops started",
347                var_labels: ["op"],
348            )),
349            retry_finished: registry.register(metric!(
350                name: "mz_persist_retry_finished_count",
351                help: "count of retry loops finished",
352                var_labels: ["op"],
353            )),
354            retry_retries: registry.register(metric!(
355                name: "mz_persist_retry_retries_count",
356                help: "count of total attempts by retry loops",
357                var_labels: ["op"],
358            )),
359            retry_sleep_seconds: registry.register(metric!(
360                name: "mz_persist_retry_sleep_seconds",
361                help: "time spent in retry loop backoff",
362                var_labels: ["op"],
363            )),
364
365            encode_count: registry.register(metric!(
366                name: "mz_persist_encode_count",
367                help: "count of op encodes",
368                var_labels: ["op"],
369            )),
370            encode_seconds: registry.register(metric!(
371                name: "mz_persist_encode_seconds",
372                help: "time spent in op encodes",
373                var_labels: ["op"],
374            )),
375            decode_count: registry.register(metric!(
376                name: "mz_persist_decode_count",
377                help: "count of op decodes",
378                var_labels: ["op"],
379            )),
380            decode_seconds: registry.register(metric!(
381                name: "mz_persist_decode_seconds",
382                help: "time spent in op decodes",
383                var_labels: ["op"],
384            )),
385
386            read_part_bytes: registry.register(metric!(
387                name: "mz_persist_read_batch_part_bytes",
388                help: "total encoded size of batch parts read",
389                var_labels: ["op"],
390            )),
391            read_part_goodbytes: registry.register(metric!(
392                name: "mz_persist_read_batch_part_goodbytes",
393                help: "total logical size of batch parts read",
394                var_labels: ["op"],
395            )),
396            read_part_count: registry.register(metric!(
397                name: "mz_persist_read_batch_part_count",
398                help: "count of batch parts read",
399                var_labels: ["op"],
400            )),
401            read_part_seconds: registry.register(metric!(
402                name: "mz_persist_read_batch_part_seconds",
403                help: "time spent reading batch parts",
404                var_labels: ["op"],
405            )),
406            read_ts_rewrite: registry.register(metric!(
407                name: "mz_persist_read_ts_rewite",
408                help: "count of updates read with rewritten ts",
409                var_labels: ["op"],
410            )),
411
412            lock_acquire_count: registry.register(metric!(
413                name: "mz_persist_lock_acquire_count",
414                help: "count of locks acquired",
415                var_labels: ["op"],
416            )),
417            lock_blocking_acquire_count: registry.register(metric!(
418                name: "mz_persist_lock_blocking_acquire_count",
419                help: "count of locks acquired that required blocking",
420                var_labels: ["op"],
421            )),
422            lock_blocking_seconds: registry.register(metric!(
423                name: "mz_persist_lock_blocking_seconds",
424                help: "time spent blocked for a lock",
425                var_labels: ["op"],
426            )),
427
428            alerts_metrics: Arc::new(AlertsMetrics::new(registry)),
429        }
430    }
431
432    fn cmds_metrics(&self, registry: &MetricsRegistry) -> CmdsMetrics {
433        CmdsMetrics {
434            init_state: self.cmd_metrics("init_state"),
435            add_rollup: self.cmd_metrics("add_rollup"),
436            remove_rollups: self.cmd_metrics("remove_rollups"),
437            upgrade_version: self.cmd_metrics("upgrade_version"),
438            register: self.cmd_metrics("register"),
439            compare_and_append: self.cmd_metrics("compare_and_append"),
440            compare_and_append_noop:             registry.register(metric!(
441                name: "mz_persist_cmd_compare_and_append_noop",
442                help: "count of compare_and_append retries that were discoverd to have already committed",
443            )),
444            compare_and_downgrade_since: self.cmd_metrics("compare_and_downgrade_since"),
445            downgrade_since: self.cmd_metrics("downgrade_since"),
446            expire_reader: self.cmd_metrics("expire_reader"),
447            expire_writer: self.cmd_metrics("expire_writer"),
448            merge_res: self.cmd_metrics("merge_res"),
449            become_tombstone: self.cmd_metrics("become_tombstone"),
450            compare_and_evolve_schema: self.cmd_metrics("compare_and_evolve_schema"),
451            spine_exert: self.cmd_metrics("spine_exert"),
452            fetch_upper_count: registry.register(metric!(
453                name: "mz_persist_cmd_fetch_upper_count",
454                help: "count of fetch_upper calls",
455            ))
456        }
457    }
458
459    fn cmd_metrics(&self, cmd: &str) -> CmdMetrics {
460        CmdMetrics {
461            name: cmd.to_owned(),
462            started: self.cmd_started.with_label_values(&[cmd]),
463            succeeded: self.cmd_succeeded.with_label_values(&[cmd]),
464            cas_mismatch: self.cmd_cas_mismatch.with_label_values(&[cmd]),
465            failed: self.cmd_failed.with_label_values(&[cmd]),
466            seconds: self.cmd_seconds.with_label_values(&[cmd]),
467        }
468    }
469
470    fn retries_metrics(&self) -> RetriesMetrics {
471        RetriesMetrics {
472            determinate: RetryDeterminate {
473                apply_unbatched_cmd_cas: self.retry_metrics("apply_unbatched_cmd::cas"),
474            },
475            external: RetryExternal {
476                batch_delete: Arc::new(self.retry_metrics("batch::delete")),
477                batch_set: self.retry_metrics("batch::set"),
478                blob_open: self.retry_metrics("blob::open"),
479                compaction_noop_delete: Arc::new(self.retry_metrics("compaction_noop::delete")),
480                consensus_open: self.retry_metrics("consensus::open"),
481                fetch_batch_get: self.retry_metrics("fetch_batch::get"),
482                fetch_state_scan: self.retry_metrics("fetch_state::scan"),
483                gc_truncate: self.retry_metrics("gc::truncate"),
484                maybe_init_cas: self.retry_metrics("maybe_init::cas"),
485                rollup_delete: self.retry_metrics("rollup::delete"),
486                rollup_get: self.retry_metrics("rollup::get"),
487                rollup_set: self.retry_metrics("rollup::set"),
488                hollow_run_get: self.retry_metrics("hollow_run::get"),
489                hollow_run_set: self.retry_metrics("hollow_run::set"),
490                storage_usage_shard_size: self.retry_metrics("storage_usage::shard_size"),
491            },
492            compare_and_append_idempotent: self.retry_metrics("compare_and_append_idempotent"),
493            fetch_latest_state: self.retry_metrics("fetch_latest_state"),
494            fetch_live_states: self.retry_metrics("fetch_live_states"),
495            idempotent_cmd: self.retry_metrics("idempotent_cmd"),
496            next_listen_batch: self.retry_metrics("next_listen_batch"),
497            snapshot: self.retry_metrics("snapshot"),
498        }
499    }
500
501    fn retry_metrics(&self, name: &str) -> RetryMetrics {
502        RetryMetrics {
503            name: name.to_owned(),
504            started: self.retry_started.with_label_values(&[name]),
505            finished: self.retry_finished.with_label_values(&[name]),
506            retries: self.retry_retries.with_label_values(&[name]),
507            sleep_seconds: self.retry_sleep_seconds.with_label_values(&[name]),
508        }
509    }
510
511    fn codecs_metrics(&self) -> CodecsMetrics {
512        CodecsMetrics {
513            state: self.codec_metrics("state"),
514            state_diff: self.codec_metrics("state_diff"),
515            batch: self.codec_metrics("batch"),
516            key: self.codec_metrics("key"),
517            val: self.codec_metrics("val"),
518        }
519    }
520
521    fn codec_metrics(&self, op: &str) -> CodecMetrics {
522        CodecMetrics {
523            encode_count: self.encode_count.with_label_values(&[op]),
524            encode_seconds: self.encode_seconds.with_label_values(&[op]),
525            decode_count: self.decode_count.with_label_values(&[op]),
526            decode_seconds: self.decode_seconds.with_label_values(&[op]),
527        }
528    }
529
530    fn blob_metrics(&self) -> BlobMetrics {
531        BlobMetrics {
532            set: self.external_op_metrics("blob_set", true),
533            get: self.external_op_metrics("blob_get", true),
534            list_keys: self.external_op_metrics("blob_list_keys", false),
535            delete: self.external_op_metrics("blob_delete", false),
536            restore: self.external_op_metrics("restore", false),
537            delete_noop: self.external_blob_delete_noop_count.clone(),
538            blob_sizes: self.external_blob_sizes.clone(),
539            rtt_latency: self.external_rtt_latency.with_label_values(&["blob"]),
540        }
541    }
542
543    fn consensus_metrics(&self) -> ConsensusMetrics {
544        ConsensusMetrics {
545            list_keys: self.external_op_metrics("consensus_list_keys", false),
546            head: self.external_op_metrics("consensus_head", false),
547            compare_and_set: self.external_op_metrics("consensus_cas", true),
548            scan: self.external_op_metrics("consensus_scan", false),
549            truncate: self.external_op_metrics("consensus_truncate", false),
550            truncated_count: self.external_consensus_truncated_count.clone(),
551            rtt_latency: self.external_rtt_latency.with_label_values(&["consensus"]),
552        }
553    }
554
555    fn external_op_metrics(&self, op: &str, latency_histogram: bool) -> ExternalOpMetrics {
556        ExternalOpMetrics {
557            started: self.external_op_started.with_label_values(&[op]),
558            succeeded: self.external_op_succeeded.with_label_values(&[op]),
559            failed: self.external_op_failed.with_label_values(&[op]),
560            bytes: self.external_op_bytes.with_label_values(&[op]),
561            seconds: self.external_op_seconds.with_label_values(&[op]),
562            seconds_histogram: if latency_histogram {
563                Some(self.external_op_latency.with_label_values(&[op]))
564            } else {
565                None
566            },
567            alerts_metrics: Arc::clone(&self.alerts_metrics),
568        }
569    }
570
571    fn batch_part_read_metrics(&self) -> BatchPartReadMetrics {
572        BatchPartReadMetrics {
573            listen: self.read_metrics("listen"),
574            snapshot: self.read_metrics("snapshot"),
575            batch_fetcher: self.read_metrics("batch_fetcher"),
576            compaction: self.read_metrics("compaction"),
577            unindexed: self.read_metrics("unindexed"),
578        }
579    }
580
581    fn read_metrics(&self, op: &str) -> ReadMetrics {
582        ReadMetrics {
583            part_bytes: self.read_part_bytes.with_label_values(&[op]),
584            part_goodbytes: self.read_part_goodbytes.with_label_values(&[op]),
585            part_count: self.read_part_count.with_label_values(&[op]),
586            seconds: self.read_part_seconds.with_label_values(&[op]),
587            ts_rewrite: self.read_ts_rewrite.with_label_values(&[op]),
588        }
589    }
590
591    fn locks_metrics(&self) -> LocksMetrics {
592        LocksMetrics {
593            applier_read_cacheable: self.lock_metrics("applier_read_cacheable"),
594            applier_read_noncacheable: self.lock_metrics("applier_read_noncacheable"),
595            applier_write: self.lock_metrics("applier_write"),
596            watch: self.lock_metrics("watch"),
597        }
598    }
599
600    fn lock_metrics(&self, op: &str) -> LockMetrics {
601        LockMetrics {
602            acquire_count: self.lock_acquire_count.with_label_values(&[op]),
603            blocking_acquire_count: self.lock_blocking_acquire_count.with_label_values(&[op]),
604            blocking_seconds: self.lock_blocking_seconds.with_label_values(&[op]),
605        }
606    }
607}
608
609#[derive(Debug)]
610pub struct CmdMetrics {
611    pub(crate) name: String,
612    pub(crate) started: IntCounter,
613    pub(crate) cas_mismatch: IntCounter,
614    pub(crate) succeeded: IntCounter,
615    pub(crate) failed: IntCounter,
616    pub(crate) seconds: Counter,
617}
618
619impl CmdMetrics {
620    pub async fn run_cmd<R, E, F, CmdFn>(
621        &self,
622        shard_metrics: &ShardMetrics,
623        cmd_fn: CmdFn,
624    ) -> Result<R, E>
625    where
626        F: std::future::Future<Output = Result<R, E>>,
627        CmdFn: FnOnce() -> F,
628    {
629        self.started.inc();
630        let start = Instant::now();
631        let res = cmd_fn().await;
632        self.seconds.inc_by(start.elapsed().as_secs_f64());
633        match res.as_ref() {
634            Ok(_) => {
635                self.succeeded.inc();
636                shard_metrics.cmd_succeeded.inc();
637            }
638            Err(_) => self.failed.inc(),
639        };
640        res
641    }
642}
643
644#[derive(Debug)]
645pub struct CmdsMetrics {
646    pub(crate) init_state: CmdMetrics,
647    pub(crate) add_rollup: CmdMetrics,
648    pub(crate) remove_rollups: CmdMetrics,
649    pub(crate) upgrade_version: CmdMetrics,
650    pub(crate) register: CmdMetrics,
651    pub(crate) compare_and_append: CmdMetrics,
652    pub(crate) compare_and_append_noop: IntCounter,
653    pub(crate) compare_and_downgrade_since: CmdMetrics,
654    pub(crate) downgrade_since: CmdMetrics,
655    pub(crate) expire_reader: CmdMetrics,
656    pub(crate) expire_writer: CmdMetrics,
657    pub(crate) merge_res: CmdMetrics,
658    pub(crate) become_tombstone: CmdMetrics,
659    pub(crate) compare_and_evolve_schema: CmdMetrics,
660    pub(crate) spine_exert: CmdMetrics,
661    pub(crate) fetch_upper_count: IntCounter,
662}
663
664#[derive(Debug)]
665pub struct RetryMetrics {
666    pub(crate) name: String,
667    pub(crate) started: IntCounter,
668    pub(crate) finished: IntCounter,
669    pub(crate) retries: IntCounter,
670    pub(crate) sleep_seconds: Counter,
671}
672
673impl RetryMetrics {
674    pub(crate) fn stream(&self, retry: RetryStream) -> MetricsRetryStream {
675        MetricsRetryStream::new(retry, self)
676    }
677}
678
679#[derive(Debug)]
680pub struct RetryDeterminate {
681    pub(crate) apply_unbatched_cmd_cas: RetryMetrics,
682}
683
684#[derive(Debug)]
685pub struct RetryExternal {
686    pub(crate) batch_delete: Arc<RetryMetrics>,
687    pub(crate) batch_set: RetryMetrics,
688    pub(crate) blob_open: RetryMetrics,
689    pub(crate) compaction_noop_delete: Arc<RetryMetrics>,
690    pub(crate) consensus_open: RetryMetrics,
691    pub(crate) fetch_batch_get: RetryMetrics,
692    pub(crate) fetch_state_scan: RetryMetrics,
693    pub(crate) gc_truncate: RetryMetrics,
694    pub(crate) maybe_init_cas: RetryMetrics,
695    pub(crate) rollup_delete: RetryMetrics,
696    pub(crate) rollup_get: RetryMetrics,
697    pub(crate) rollup_set: RetryMetrics,
698    pub(crate) hollow_run_get: RetryMetrics,
699    pub(crate) hollow_run_set: RetryMetrics,
700    pub(crate) storage_usage_shard_size: RetryMetrics,
701}
702
703#[derive(Debug)]
704pub struct RetriesMetrics {
705    pub(crate) determinate: RetryDeterminate,
706    pub(crate) external: RetryExternal,
707
708    pub(crate) compare_and_append_idempotent: RetryMetrics,
709    pub(crate) fetch_latest_state: RetryMetrics,
710    pub(crate) fetch_live_states: RetryMetrics,
711    pub(crate) idempotent_cmd: RetryMetrics,
712    pub(crate) next_listen_batch: RetryMetrics,
713    pub(crate) snapshot: RetryMetrics,
714}
715
716#[derive(Debug)]
717pub struct BatchPartReadMetrics {
718    pub(crate) listen: ReadMetrics,
719    pub(crate) snapshot: ReadMetrics,
720    pub(crate) batch_fetcher: ReadMetrics,
721    pub(crate) compaction: ReadMetrics,
722    pub(crate) unindexed: ReadMetrics,
723}
724
725#[derive(Debug, Clone)]
726pub struct ReadMetrics {
727    pub(crate) part_bytes: IntCounter,
728    pub(crate) part_goodbytes: IntCounter,
729    pub(crate) part_count: IntCounter,
730    pub(crate) seconds: Counter,
731    pub(crate) ts_rewrite: IntCounter,
732}
733
734// This one is Clone in contrast to the others because it has to get moved into
735// a task.
736#[derive(Debug, Clone)]
737pub struct BatchWriteMetrics {
738    pub(crate) bytes: IntCounter,
739    pub(crate) goodbytes: IntCounter,
740    pub(crate) seconds: Counter,
741    pub(crate) write_stalls: IntCounter,
742    pub(crate) key_lower_too_big: IntCounter,
743
744    pub(crate) unordered: IntCounter,
745    pub(crate) codec_order: IntCounter,
746    pub(crate) structured_order: IntCounter,
747    _order_counts: IntCounterVec,
748
749    pub(crate) step_stats: Counter,
750    pub(crate) step_part_writing: Counter,
751    pub(crate) step_inline: Counter,
752}
753
754impl BatchWriteMetrics {
755    fn new(registry: &MetricsRegistry, name: &str) -> Self {
756        let order_counts: IntCounterVec = registry.register(metric!(
757                name: format!("mz_persist_{}_write_batch_order", name),
758                help: "count of batches by the data ordering",
759                var_labels: ["order"],
760        ));
761        let unordered = order_counts.with_label_values(&["unordered"]);
762        let codec_order = order_counts.with_label_values(&["codec"]);
763        let structured_order = order_counts.with_label_values(&["structured"]);
764
765        BatchWriteMetrics {
766            bytes: registry.register(metric!(
767                name: format!("mz_persist_{}_bytes", name),
768                help: format!("total encoded size of {} batches written", name),
769            )),
770            goodbytes: registry.register(metric!(
771                name: format!("mz_persist_{}_goodbytes", name),
772                help: format!("total logical size of {} batches written", name),
773            )),
774            seconds: registry.register(metric!(
775                name: format!("mz_persist_{}_write_batch_part_seconds", name),
776                help: format!("time spent writing {} batches", name),
777            )),
778            write_stalls: registry.register(metric!(
779                name: format!("mz_persist_{}_write_stall_count", name),
780                help: format!(
781                    "count of {} writes stalling to await max outstanding reqs",
782                    name
783                ),
784            )),
785            key_lower_too_big: registry.register(metric!(
786                name: format!("mz_persist_{}_key_lower_too_big", name),
787                help: format!(
788                    "count of {} writes that were unable to write a key lower, because the size threshold was too low",
789                    name
790                ),
791            )),
792            unordered,
793            codec_order,
794            structured_order,
795            _order_counts: order_counts,
796            step_stats: registry.register(metric!(
797                name: format!("mz_persist_{}_step_stats", name),
798                help: format!("time spent computing {} update stats", name),
799            )),
800            step_part_writing: registry.register(metric!(
801                name: format!("mz_persist_{}_step_part_writing", name),
802                help: format!("blocking time spent writing parts for {} updates", name),
803            )),
804            step_inline: registry.register(metric!(
805                name: format!("mz_persist_{}_step_inline", name),
806                help: format!("time spent encoding {} inline batches", name)
807            )),
808        }
809    }
810}
811
812#[derive(Debug)]
813pub struct CompactionMetrics {
814    pub(crate) requested: IntCounter,
815    pub(crate) dropped: IntCounter,
816    pub(crate) disabled: IntCounter,
817    pub(crate) skipped: IntCounter,
818    pub(crate) started: IntCounter,
819    pub(crate) applied: IntCounter,
820    pub(crate) timed_out: IntCounter,
821    pub(crate) failed: IntCounter,
822    pub(crate) noop: IntCounter,
823    pub(crate) seconds: Counter,
824    pub(crate) concurrency_waits: IntCounter,
825    pub(crate) queued_seconds: Counter,
826    pub(crate) memory_violations: IntCounter,
827    pub(crate) runs_compacted: IntCounter,
828    pub(crate) chunks_compacted: IntCounter,
829    pub(crate) not_all_prefetched: IntCounter,
830    pub(crate) parts_prefetched: IntCounter,
831    pub(crate) parts_waited: IntCounter,
832    pub(crate) fast_path_eligible: IntCounter,
833    pub(crate) admin_count: IntCounter,
834
835    pub(crate) applied_exact_match: IntCounter,
836    pub(crate) applied_subset_match: IntCounter,
837    pub(crate) not_applied_too_many_updates: IntCounter,
838
839    pub(crate) batch: BatchWriteMetrics,
840    pub(crate) steps: CompactionStepTimings,
841    pub(crate) schema_selection: CompactionSchemaSelection,
842
843    pub(crate) _steps_vec: CounterVec,
844}
845
846impl CompactionMetrics {
847    fn new(registry: &MetricsRegistry) -> Self {
848        let step_timings: CounterVec = registry.register(metric!(
849                name: "mz_persist_compaction_step_seconds",
850                help: "time spent on individual steps of compaction",
851                var_labels: ["step"],
852        ));
853        let schema_selection: CounterVec = registry.register(metric!(
854            name: "mz_persist_compaction_schema_selection",
855            help: "count of compactions and how we did schema selection",
856            var_labels: ["selection"],
857        ));
858
859        CompactionMetrics {
860            requested: registry.register(metric!(
861                name: "mz_persist_compaction_requested",
862                help: "count of total compaction requests",
863            )),
864            dropped: registry.register(metric!(
865                name: "mz_persist_compaction_dropped",
866                help: "count of total compaction requests dropped due to a full queue",
867            )),
868            disabled: registry.register(metric!(
869                name: "mz_persist_compaction_disabled",
870                help: "count of total compaction requests dropped because compaction was disabled",
871            )),
872            skipped: registry.register(metric!(
873                name: "mz_persist_compaction_skipped",
874                help: "count of compactions skipped due to heuristics",
875            )),
876            started: registry.register(metric!(
877                name: "mz_persist_compaction_started",
878                help: "count of compactions started",
879            )),
880            failed: registry.register(metric!(
881                name: "mz_persist_compaction_failed",
882                help: "count of compactions failed",
883            )),
884            applied: registry.register(metric!(
885                name: "mz_persist_compaction_applied",
886                help: "count of compactions applied to state",
887            )),
888            timed_out: registry.register(metric!(
889                name: "mz_persist_compaction_timed_out",
890                help: "count of compactions that timed out",
891            )),
892            noop: registry.register(metric!(
893                name: "mz_persist_compaction_noop",
894                help: "count of compactions discarded (obsolete)",
895            )),
896            seconds: registry.register(metric!(
897                name: "mz_persist_compaction_seconds",
898                help: "time spent in compaction",
899            )),
900            concurrency_waits: registry.register(metric!(
901                name: "mz_persist_compaction_concurrency_waits",
902                help: "count of compaction requests that ever blocked due to concurrency limit",
903            )),
904            queued_seconds: registry.register(metric!(
905                name: "mz_persist_compaction_queued_seconds",
906                help: "time that compaction requests spent queued",
907            )),
908            memory_violations: registry.register(metric!(
909                name: "mz_persist_compaction_memory_violations",
910                help: "count of compaction memory requirement violations",
911            )),
912            runs_compacted: registry.register(metric!(
913                name: "mz_persist_compaction_runs_compacted",
914                help: "count of runs compacted",
915            )),
916            chunks_compacted: registry.register(metric!(
917                name: "mz_persist_compaction_chunks_compacted",
918                help: "count of run chunks compacted",
919            )),
920            not_all_prefetched: registry.register(metric!(
921                name: "mz_persist_compaction_not_all_prefetched",
922                help: "count of compactions where not all inputs were prefetched",
923            )),
924            parts_prefetched: registry.register(metric!(
925                name: "mz_persist_compaction_parts_prefetched",
926                help: "count of compaction parts completely prefetched by the time they're needed",
927            )),
928            parts_waited: registry.register(metric!(
929                name: "mz_persist_compaction_parts_waited",
930                help: "count of compaction parts that had to be waited on",
931            )),
932            fast_path_eligible: registry.register(metric!(
933                name: "mz_persist_compaction_fast_path_eligible",
934                help: "count of compaction requests that could have used the fast-path optimization",
935            )),
936            admin_count: registry.register(metric!(
937                name: "mz_persist_compaction_admin_count",
938                help: "count of compaction requests that were performed by admin tooling",
939            )),
940            applied_exact_match: registry.register(metric!(
941                name: "mz_persist_compaction_applied_exact_match",
942                help: "count of merge results that exactly replaced a SpineBatch",
943            )),
944            applied_subset_match: registry.register(metric!(
945                name: "mz_persist_compaction_applied_subset_match",
946                help: "count of merge results that replaced a subset of a SpineBatch",
947            )),
948            not_applied_too_many_updates: registry.register(metric!(
949                name: "mz_persist_compaction_not_applied_too_many_updates",
950                help: "count of merge results that did not apply due to too many updates",
951            )),
952            batch: BatchWriteMetrics::new(registry, "compaction"),
953            steps: CompactionStepTimings::new(step_timings.clone()),
954            schema_selection: CompactionSchemaSelection::new(schema_selection.clone()),
955            _steps_vec: step_timings,
956        }
957    }
958}
959
960#[derive(Debug)]
961pub struct CompactionStepTimings {
962    pub(crate) part_fetch_seconds: Counter,
963    pub(crate) heap_population_seconds: Counter,
964}
965
966impl CompactionStepTimings {
967    fn new(step_timings: CounterVec) -> CompactionStepTimings {
968        CompactionStepTimings {
969            part_fetch_seconds: step_timings.with_label_values(&["part_fetch"]),
970            heap_population_seconds: step_timings.with_label_values(&["heap_population"]),
971        }
972    }
973}
974
975#[derive(Debug)]
976pub struct CompactionSchemaSelection {
977    pub(crate) recent_schema: Counter,
978    pub(crate) no_schema: Counter,
979}
980
981impl CompactionSchemaSelection {
982    fn new(schema_selection: CounterVec) -> CompactionSchemaSelection {
983        CompactionSchemaSelection {
984            recent_schema: schema_selection.with_label_values(&["recent"]),
985            no_schema: schema_selection.with_label_values(&["none"]),
986        }
987    }
988}
989
990#[derive(Debug)]
991pub struct GcMetrics {
992    pub(crate) noop: IntCounter,
993    pub(crate) started: IntCounter,
994    pub(crate) finished: IntCounter,
995    pub(crate) merged: IntCounter,
996    pub(crate) seconds: Counter,
997    pub(crate) steps: GcStepTimings,
998}
999
1000#[derive(Debug)]
1001pub struct GcStepTimings {
1002    pub(crate) find_removable_rollups: Counter,
1003    pub(crate) fetch_seconds: Counter,
1004    pub(crate) find_deletable_blobs_seconds: Counter,
1005    pub(crate) delete_rollup_seconds: Counter,
1006    pub(crate) delete_batch_part_seconds: Counter,
1007    pub(crate) truncate_diff_seconds: Counter,
1008    pub(crate) remove_rollups_from_state: Counter,
1009    pub(crate) post_gc_calculations_seconds: Counter,
1010}
1011
1012impl GcStepTimings {
1013    fn new(step_timings: CounterVec) -> Self {
1014        Self {
1015            find_removable_rollups: step_timings.with_label_values(&["find_removable_rollups"]),
1016            fetch_seconds: step_timings.with_label_values(&["fetch"]),
1017            find_deletable_blobs_seconds: step_timings.with_label_values(&["find_deletable_blobs"]),
1018            delete_rollup_seconds: step_timings.with_label_values(&["delete_rollup"]),
1019            delete_batch_part_seconds: step_timings.with_label_values(&["delete_batch_part"]),
1020            truncate_diff_seconds: step_timings.with_label_values(&["truncate_diff"]),
1021            remove_rollups_from_state: step_timings
1022                .with_label_values(&["remove_rollups_from_state"]),
1023            post_gc_calculations_seconds: step_timings.with_label_values(&["post_gc_calculations"]),
1024        }
1025    }
1026}
1027
1028impl GcMetrics {
1029    fn new(registry: &MetricsRegistry) -> Self {
1030        let step_timings: CounterVec = registry.register(metric!(
1031                name: "mz_persist_gc_step_seconds",
1032                help: "time spent on individual steps of gc",
1033                var_labels: ["step"],
1034        ));
1035        GcMetrics {
1036            noop: registry.register(metric!(
1037                name: "mz_persist_gc_noop",
1038                help: "count of garbage collections skipped because they were already done",
1039            )),
1040            started: registry.register(metric!(
1041                name: "mz_persist_gc_started",
1042                help: "count of garbage collections started",
1043            )),
1044            finished: registry.register(metric!(
1045                name: "mz_persist_gc_finished",
1046                help: "count of garbage collections finished",
1047            )),
1048            merged: registry.register(metric!(
1049                name: "mz_persist_gc_merged_reqs",
1050                help: "count of garbage collection requests merged",
1051            )),
1052            seconds: registry.register(metric!(
1053                name: "mz_persist_gc_seconds",
1054                help: "time spent in garbage collections",
1055            )),
1056            steps: GcStepTimings::new(step_timings),
1057        }
1058    }
1059}
1060
1061#[derive(Debug)]
1062pub struct LeaseMetrics {
1063    pub(crate) timeout_read: IntCounter,
1064    /// Counted where `shard_source` reports a lost lease. Only observable where
1065    /// the error handler survives it (storage); compute halts right after.
1066    pub(crate) expired_live_read: IntCounter,
1067    pub(crate) dropped_part: IntCounter,
1068}
1069
1070impl LeaseMetrics {
1071    fn new(registry: &MetricsRegistry) -> Self {
1072        LeaseMetrics {
1073            timeout_read: registry.register(metric!(
1074                name: "mz_persist_lease_timeout_read",
1075                help: "count of readers whose lease timed out",
1076            )),
1077            expired_live_read: registry.register(metric!(
1078                name: "mz_persist_lease_expired_live_read",
1079                help: "count of fetches that found their reader's lease had timed out",
1080            )),
1081            dropped_part: registry.register(metric!(
1082                name: "mz_persist_lease_dropped_part",
1083                help: "count of LeasedBatchParts that were dropped without being politely returned",
1084            )),
1085        }
1086    }
1087}
1088
1089struct IncOnDrop(IntCounter);
1090
1091impl Drop for IncOnDrop {
1092    fn drop(&mut self) {
1093        self.0.inc()
1094    }
1095}
1096
1097pub struct MetricsRetryStream {
1098    retry: RetryStream,
1099    pub(crate) retries: IntCounter,
1100    sleep_seconds: Counter,
1101    _finished: IncOnDrop,
1102}
1103
1104impl MetricsRetryStream {
1105    pub fn new(retry: RetryStream, metrics: &RetryMetrics) -> Self {
1106        metrics.started.inc();
1107        MetricsRetryStream {
1108            retry,
1109            retries: metrics.retries.clone(),
1110            sleep_seconds: metrics.sleep_seconds.clone(),
1111            _finished: IncOnDrop(metrics.finished.clone()),
1112        }
1113    }
1114
1115    /// How many times [Self::sleep] has been called.
1116    pub fn attempt(&self) -> usize {
1117        self.retry.attempt()
1118    }
1119
1120    /// The next sleep (without jitter for easy printing in logs).
1121    pub fn next_sleep(&self) -> Duration {
1122        self.retry.next_sleep()
1123    }
1124
1125    /// Executes the next sleep in the series.
1126    ///
1127    /// This isn't cancel-safe, so it consumes and returns self, to prevent
1128    /// accidental mis-use.
1129    pub async fn sleep(self) -> Self {
1130        self.retries.inc();
1131        self.sleep_seconds
1132            .inc_by(self.retry.next_sleep().as_secs_f64());
1133        let retry = self.retry.sleep().await;
1134        MetricsRetryStream {
1135            retry,
1136            retries: self.retries,
1137            sleep_seconds: self.sleep_seconds,
1138            _finished: self._finished,
1139        }
1140    }
1141}
1142
1143#[derive(Debug)]
1144pub struct CodecsMetrics {
1145    pub(crate) state: CodecMetrics,
1146    pub(crate) state_diff: CodecMetrics,
1147    pub(crate) batch: CodecMetrics,
1148    pub(crate) key: CodecMetrics,
1149    pub(crate) val: CodecMetrics,
1150    // Intentionally not adding time and diff because they're just
1151    // `{to,from}_le_bytes`.
1152}
1153
1154#[derive(Debug)]
1155pub struct CodecMetrics {
1156    pub(crate) encode_count: IntCounter,
1157    pub(crate) encode_seconds: Counter,
1158    pub(crate) decode_count: IntCounter,
1159    pub(crate) decode_seconds: Counter,
1160}
1161
1162impl CodecMetrics {
1163    pub(crate) fn encode<R, F: FnOnce() -> R>(&self, f: F) -> R {
1164        let now = Instant::now();
1165        let r = f();
1166        self.encode_count.inc();
1167        self.encode_seconds.inc_by(now.elapsed().as_secs_f64());
1168        r
1169    }
1170
1171    pub(crate) fn decode<R, F: FnOnce() -> R>(&self, f: F) -> R {
1172        let now = Instant::now();
1173        let r = f();
1174        self.decode_count.inc();
1175        self.decode_seconds.inc_by(now.elapsed().as_secs_f64());
1176        r
1177    }
1178}
1179
1180#[derive(Debug)]
1181pub struct StateMetrics {
1182    pub(crate) apply_spine_fast_path: IntCounter,
1183    pub(crate) apply_spine_slow_path: IntCounter,
1184    pub(crate) apply_spine_slow_path_lenient: IntCounter,
1185    pub(crate) apply_spine_slow_path_lenient_adjustment: IntCounter,
1186    pub(crate) apply_spine_slow_path_with_reconstruction: IntCounter,
1187    pub(crate) apply_spine_flattened: IntCounter,
1188    pub(crate) update_state_noop_path: IntCounter,
1189    pub(crate) update_state_empty_path: IntCounter,
1190    pub(crate) update_state_fast_path: IntCounter,
1191    pub(crate) update_state_slow_path: IntCounter,
1192    pub(crate) rollup_at_seqno_migration: IntCounter,
1193    pub(crate) fetch_recent_live_diffs_fast_path: IntCounter,
1194    pub(crate) fetch_recent_live_diffs_slow_path: IntCounter,
1195    pub(crate) writer_added: IntCounter,
1196    pub(crate) writer_removed: IntCounter,
1197    pub(crate) force_apply_hostname: IntCounter,
1198    pub(crate) rollup_write_success: IntCounter,
1199    pub(crate) rollup_write_noop_latest: IntCounter,
1200    pub(crate) rollup_write_noop_truncated: IntCounter,
1201}
1202
1203impl StateMetrics {
1204    pub(crate) fn new(registry: &MetricsRegistry) -> Self {
1205        let rollup_write_noop: IntCounterVec = registry.register(metric!(
1206                name: "mz_persist_state_rollup_write_noop",
1207                help: "count of no-op rollup writes",
1208                var_labels: ["reason"],
1209        ));
1210
1211        StateMetrics {
1212            apply_spine_fast_path: registry.register(metric!(
1213                name: "mz_persist_state_apply_spine_fast_path",
1214                help: "count of spine diff applications that hit the fast path",
1215            )),
1216            apply_spine_slow_path: registry.register(metric!(
1217                name: "mz_persist_state_apply_spine_slow_path",
1218                help: "count of spine diff applications that hit the slow path",
1219            )),
1220            apply_spine_slow_path_lenient: registry.register(metric!(
1221                name: "mz_persist_state_apply_spine_slow_path_lenient",
1222                help: "count of spine diff applications that hit the lenient compaction apply path",
1223            )),
1224            apply_spine_slow_path_lenient_adjustment: registry.register(metric!(
1225                name: "mz_persist_state_apply_spine_slow_path_lenient_adjustment",
1226                help: "count of adjustments made by the lenient compaction apply path",
1227            )),
1228            apply_spine_slow_path_with_reconstruction: registry.register(metric!(
1229                name: "mz_persist_state_apply_spine_slow_path_with_reconstruction",
1230                help: "count of spine diff applications that hit the slow path with extra spine reconstruction step",
1231            )),
1232            apply_spine_flattened: registry.register(metric!(
1233                name: "mz_persist_state_apply_spine_flattened",
1234                help: "count of spine diff applications that flatten the trace",
1235            )),
1236            update_state_noop_path: registry.register(metric!(
1237                name: "mz_persist_state_update_state_noop_path",
1238                help: "count of state update applications that no-oped due to shared state",
1239            )),
1240            update_state_empty_path: registry.register(metric!(
1241                name: "mz_persist_state_update_state_empty_path",
1242                help: "count of state update applications that found no new updates",
1243            )),
1244            update_state_fast_path: registry.register(metric!(
1245                name: "mz_persist_state_update_state_fast_path",
1246                help: "count of state update applications that hit the fast path",
1247            )),
1248            update_state_slow_path: registry.register(metric!(
1249                name: "mz_persist_state_update_state_slow_path",
1250                help: "count of state update applications that hit the slow path",
1251            )),
1252            rollup_at_seqno_migration: registry.register(metric!(
1253                name: "mz_persist_state_rollup_at_seqno_migration",
1254                help: "count of fetch_rollup_at_seqno calls that only worked because of the migration",
1255            )),
1256            fetch_recent_live_diffs_fast_path: registry.register(metric!(
1257                name: "mz_persist_state_fetch_recent_live_diffs_fast_path",
1258                help: "count of fetch_recent_live_diffs that hit the fast path",
1259            )),
1260            fetch_recent_live_diffs_slow_path: registry.register(metric!(
1261                name: "mz_persist_state_fetch_recent_live_diffs_slow_path",
1262                help: "count of fetch_recent_live_diffs that hit the slow path",
1263            )),
1264            writer_added: registry.register(metric!(
1265                name: "mz_persist_state_writer_added",
1266                help: "count of writers added to the state",
1267            )),
1268            writer_removed: registry.register(metric!(
1269                name: "mz_persist_state_writer_removed",
1270                help: "count of writers removed from the state",
1271            )),
1272            force_apply_hostname: registry.register(metric!(
1273                name: "mz_persist_state_force_applied_hostname",
1274                help: "count of when hostname diffs needed to be force applied",
1275            )),
1276            rollup_write_success: registry.register(metric!(
1277                name: "mz_persist_state_rollup_write_success",
1278                help: "count of rollups written successful (may not be linked in to state)",
1279            )),
1280            rollup_write_noop_latest: rollup_write_noop.with_label_values(&["latest"]),
1281            rollup_write_noop_truncated: rollup_write_noop.with_label_values(&["truncated"]),
1282        }
1283    }
1284}
1285
1286#[derive(Debug)]
1287pub struct ShardsMetrics {
1288    // Unlike all the other metrics in here, ShardsMetrics intentionally uses
1289    // the DeleteOnDrop wrappers. A process might stop using a shard (drop all
1290    // handles to it) but e.g. the set of commands never changes.
1291    //
1292    // Every per-shard family also has a bounded aggregate whose cardinality
1293    // does not grow with the shard count. `PER_SHARD_FAMILIES` lists which.
1294    //
1295    // The scrape-time aggregates, plus `mz_persist_shard_count` and
1296    // `mz_persist_stale_shard_count`, are not fields here: they live in a
1297    // `ShardsAggregateMetrics` collector that the registry owns, sharing this
1298    // struct's `shards` map so one scrape walk feeds all of them.
1299    encoded_rollup_size: mz_ore::metrics::UIntGaugeVec,
1300    encoded_diff_size: mz_ore::metrics::IntCounterVec,
1301    hollow_batch_count: mz_ore::metrics::UIntGaugeVec,
1302    spine_batch_count: mz_ore::metrics::UIntGaugeVec,
1303    batch_part_count: mz_ore::metrics::UIntGaugeVec,
1304    batch_part_version_count: mz_ore::metrics::UIntGaugeVec,
1305    update_count: mz_ore::metrics::UIntGaugeVec,
1306    rollup_count: mz_ore::metrics::UIntGaugeVec,
1307    largest_batch_size: mz_ore::metrics::UIntGaugeVec,
1308    seqnos_held: mz_ore::metrics::UIntGaugeVec,
1309    seqnos_since_last_rollup: mz_ore::metrics::UIntGaugeVec,
1310    gc_seqno_held_parts: mz_ore::metrics::UIntGaugeVec,
1311    gc_live_diffs: mz_ore::metrics::UIntGaugeVec,
1312    gc_finished: mz_ore::metrics::IntCounterVec,
1313    compaction_applied: mz_ore::metrics::IntCounterVec,
1314    cmd_succeeded: mz_ore::metrics::IntCounterVec,
1315    usage_current_state_batches_bytes: mz_ore::metrics::UIntGaugeVec,
1316    usage_current_state_rollups_bytes: mz_ore::metrics::UIntGaugeVec,
1317    usage_referenced_not_current_state_bytes: mz_ore::metrics::UIntGaugeVec,
1318    usage_not_leaked_not_referenced_bytes: mz_ore::metrics::UIntGaugeVec,
1319    usage_leaked_bytes: mz_ore::metrics::UIntGaugeVec,
1320    blob_gets: mz_ore::metrics::IntCounterVec,
1321    blob_sets: mz_ore::metrics::IntCounterVec,
1322    unconsolidated_snapshot: mz_ore::metrics::IntCounterVec,
1323    inline_part_count: UIntGaugeVec,
1324    compact_batches: UIntGaugeVec,
1325    compacting_batches: UIntGaugeVec,
1326    noncompact_batches: UIntGaugeVec,
1327    process_blob_gets: IntCounter,
1328    process_blob_sets: IntCounter,
1329    process_encoded_diff_size: IntCounter,
1330    process_unconsolidated_snapshot: IntCounter,
1331    // We hand out `Arc<ShardMetrics>` to read and write handles, but store it
1332    // here as `Weak`. This allows us to discover if it's no longer in use and
1333    // so we can remove it from the map.
1334    shards: Arc<Mutex<BTreeMap<ShardId, Weak<ShardMetrics>>>>,
1335}
1336
1337/// Removes the per-shard series this process should not export from gathered
1338/// families, as configured by [`SHARD_METRICS`] and
1339/// [`PER_SHARD_METRICS_ENABLE_REGEX`].
1340///
1341/// It matches the names in `PER_SHARD_FAMILIES`, not a name prefix, because
1342/// `mz_persist_shard_count`, `mz_persist_stale_shard_count` and the
1343/// `mz_persist_shard_*_percentile` / `_sum` / `_topk` aggregates and
1344/// `mz_persist_shard_info` share the prefix.
1345///
1346/// NOTE: Postprocessors run after collection, so the `invalid_*` counters land
1347/// in the scrape after the one that bumped them.
1348struct PerShardMetricsFilter {
1349    configs: Arc<ConfigSet>,
1350    /// The last seen value of `SHARD_METRICS` and what it parsed to.
1351    mode: (String, ShardMetricsExport),
1352    invalid_mode: IntCounter,
1353    /// The last seen value of `PER_SHARD_METRICS_ENABLE_REGEX`, and its
1354    /// compiled form if it is non-empty and valid.
1355    allow: (String, Option<Regex>),
1356    invalid_regex: IntCounter,
1357}
1358
1359impl PerShardMetricsFilter {
1360    fn filter(&mut self, families: &mut Vec<MetricFamily>) {
1361        if self.mode().per_shard() {
1362            return;
1363        }
1364        let allow = self.allow_regex();
1365        families.retain_mut(|f| {
1366            if !PER_SHARD_FAMILIES.iter().any(|p| p.name == f.name()) {
1367                return true;
1368            }
1369            let Some(allow) = allow else {
1370                return false;
1371            };
1372            f.mut_metric().retain(|m| {
1373                m.get_label()
1374                    .iter()
1375                    .any(|l| matches!(l.name(), "shard" | "name") && allow.is_match(l.value()))
1376            });
1377            !f.get_metric().is_empty()
1378        });
1379    }
1380
1381    /// Returns the regex of series to keep, or `None` to keep none.
1382    ///
1383    /// Recompiles only when the config value changes, so an invalid value is
1384    /// logged once, but it is counted on every scrape. It fails closed: an
1385    /// invalid regex keeps nothing rather than everything, which would bring
1386    /// back the full per-shard cardinality.
1387    fn allow_regex(&mut self) -> Option<&Regex> {
1388        let value = PER_SHARD_METRICS_ENABLE_REGEX.get(&self.configs);
1389        if value != self.allow.0 {
1390            let compiled = if value.is_empty() {
1391                None
1392            } else {
1393                Regex::new(&value)
1394                    .inspect_err(|err| {
1395                        warn!(
1396                            "invalid {}, keeping no per-shard series: {err}",
1397                            PER_SHARD_METRICS_ENABLE_REGEX.name()
1398                        )
1399                    })
1400                    .ok()
1401            };
1402            self.allow = (value, compiled);
1403        }
1404        if !self.allow.0.is_empty() && self.allow.1.is_none() {
1405            self.invalid_regex.inc();
1406        }
1407        self.allow.1.as_ref()
1408    }
1409
1410    /// The export mode, or the default if the config value is unrecognized.
1411    ///
1412    /// Reparses only on change, so an unrecognized value logs once but counts
1413    /// every scrape.
1414    fn mode(&mut self) -> ShardMetricsExport {
1415        let value = SHARD_METRICS.get(&self.configs);
1416        if value != self.mode.0 {
1417            let parsed = ShardMetricsExport::parse(&value).unwrap_or_else(|| {
1418                let default = ShardMetricsExport::default();
1419                warn!(
1420                    "unrecognized {}: '{value}', exporting '{}'",
1421                    SHARD_METRICS.name(),
1422                    default.as_str()
1423                );
1424                default
1425            });
1426            self.mode = (value, parsed);
1427        }
1428        if self.mode.1.as_str() != self.mode.0 {
1429            self.invalid_mode.inc();
1430        }
1431        self.mode.1
1432    }
1433}
1434
1435impl ShardsMetrics {
1436    fn new(registry: &MetricsRegistry, configs: Arc<ConfigSet>) -> Self {
1437        let shards = Arc::new(Mutex::new(BTreeMap::new()));
1438        registry.register_collector(ShardsAggregateMetrics::new(Arc::clone(&shards), configs));
1439        ShardsMetrics {
1440            encoded_rollup_size: registry.register(metric!(
1441                name: "mz_persist_shard_rollup_size_bytes",
1442                help: "total encoded rollup size by shard",
1443                var_labels: ["shard", "name"],
1444            )),
1445            encoded_diff_size: registry.register(metric!(
1446                name: "mz_persist_shard_diff_size_bytes",
1447                help: "total encoded diff size by shard",
1448                var_labels: ["shard", "name"],
1449            )),
1450            hollow_batch_count: registry.register(metric!(
1451                name: "mz_persist_shard_hollow_batch_count",
1452                help: "count of hollow batches by shard",
1453                var_labels: ["shard", "name"],
1454            )),
1455            spine_batch_count: registry.register(metric!(
1456                name: "mz_persist_shard_spine_batch_count",
1457                help: "count of spine batches by shard",
1458                var_labels: ["shard", "name"],
1459            )),
1460            batch_part_count: registry.register(metric!(
1461                name: "mz_persist_shard_batch_part_count",
1462                help: "count of batch parts by shard",
1463                var_labels: ["shard", "name"],
1464            )),
1465            batch_part_version_count: registry.register(metric!(
1466                name: "mz_persist_shard_batch_part_version_count",
1467                help: "count of batch parts by shard and version",
1468                var_labels: ["shard", "name", "version"],
1469            )),
1470            update_count: registry.register(metric!(
1471                name: "mz_persist_shard_update_count",
1472                help: "count of updates by shard",
1473                var_labels: ["shard", "name"],
1474            )),
1475            rollup_count: registry.register(metric!(
1476                name: "mz_persist_shard_rollup_count",
1477                help: "count of rollups by shard",
1478                var_labels: ["shard", "name"],
1479            )),
1480            largest_batch_size: registry.register(metric!(
1481                name: "mz_persist_shard_largest_batch_size",
1482                help: "largest encoded batch size by shard",
1483                var_labels: ["shard", "name"],
1484            )),
1485            seqnos_held: registry.register(metric!(
1486                name: "mz_persist_shard_seqnos_held",
1487                help: "maximum count of gc-ineligible states by shard",
1488                var_labels: ["shard", "name"],
1489            )),
1490            seqnos_since_last_rollup: registry.register(metric!(
1491                name: "mz_persist_shard_seqnos_since_last_rollup",
1492                help: "count of seqnos since last rollup",
1493                var_labels: ["shard", "name"],
1494            )),
1495            gc_seqno_held_parts: registry.register(metric!(
1496                name: "mz_persist_shard_gc_seqno_held_parts",
1497                help: "count of parts referenced by some live state but not the current state (ie. parts kept only to satisfy seqno holds) at GC time",
1498                var_labels: ["shard", "name"],
1499            )),
1500            gc_live_diffs: registry.register(metric!(
1501                name: "mz_persist_shard_gc_live_diffs",
1502                help: "the number of diffs (or, alternatively, the number of seqnos) present in consensus state at GC time",
1503                var_labels: ["shard", "name"],
1504            )),
1505            gc_finished: registry.register(metric!(
1506                name: "mz_persist_shard_gc_finished",
1507                help: "count of garbage collections finished by shard",
1508                var_labels: ["shard", "name"],
1509            )),
1510            compaction_applied: registry.register(metric!(
1511                name: "mz_persist_shard_compaction_applied",
1512                help: "count of compactions applied to state by shard",
1513                var_labels: ["shard", "name"],
1514            )),
1515            cmd_succeeded: registry.register(metric!(
1516                name: "mz_persist_shard_cmd_succeeded",
1517                help: "count of commands succeeded by shard",
1518                var_labels: ["shard", "name"],
1519            )),
1520            usage_current_state_batches_bytes: registry.register(metric!(
1521                name: "mz_persist_shard_usage_current_state_batches_bytes",
1522                help: "data in batches/parts referenced by current version of state",
1523                var_labels: ["shard", "name"],
1524            )),
1525            usage_current_state_rollups_bytes: registry.register(metric!(
1526                name: "mz_persist_shard_usage_current_state_rollups_bytes",
1527                help: "data in rollups referenced by current version of state",
1528                var_labels: ["shard", "name"],
1529            )),
1530            usage_referenced_not_current_state_bytes: registry.register(metric!(
1531                name: "mz_persist_shard_usage_referenced_not_current_state_bytes",
1532                help: "data referenced only by a previous version of state",
1533                var_labels: ["shard", "name"],
1534            )),
1535            usage_not_leaked_not_referenced_bytes: registry.register(metric!(
1536                name: "mz_persist_shard_usage_not_leaked_not_referenced_bytes",
1537                help: "data written by an active writer but not referenced by any version of state",
1538                var_labels: ["shard", "name"],
1539            )),
1540            usage_leaked_bytes: registry.register(metric!(
1541                name: "mz_persist_shard_usage_leaked_bytes",
1542                help: "data reclaimable by a leaked blob detector",
1543                var_labels: ["shard", "name"],
1544            )),
1545            blob_gets: registry.register(metric!(
1546                name: "mz_persist_shard_blob_gets",
1547                help: "number of Blob::get calls for this shard",
1548                var_labels: ["shard", "name"],
1549            )),
1550            blob_sets: registry.register(metric!(
1551                name: "mz_persist_shard_blob_sets",
1552                help: "number of Blob::set calls for this shard",
1553                var_labels: ["shard", "name"],
1554            )),
1555            unconsolidated_snapshot: registry.register(metric!(
1556                name: "mz_persist_shard_unconsolidated_snapshot",
1557                help: "in snapshot_and_read, the number of times consolidating the raw data wasn't enough to produce consolidated output",
1558                var_labels: ["shard", "name"],
1559            )),
1560            inline_part_count: registry.register(metric!(
1561                name: "mz_persist_shard_inline_part_count",
1562                help: "count of parts inline in shard metadata",
1563                var_labels: ["shard", "name"],
1564            )),
1565            compact_batches: registry.register(metric!(
1566                name: "mz_persist_shard_compact_batches",
1567                help: "number of fully compact batches in the shard",
1568                var_labels: ["shard", "name"],
1569            )),
1570            compacting_batches: registry.register(metric!(
1571                name: "mz_persist_shard_compacting_batches",
1572                help: "number of batches in the shard with compactions in progress",
1573                var_labels: ["shard", "name"],
1574            )),
1575            noncompact_batches: registry.register(metric!(
1576                name: "mz_persist_shard_noncompact_batches",
1577                help: "number of batches in the shard that aren't compact and have no ongoing compaction",
1578                var_labels: ["shard", "name"],
1579            )),
1580            process_blob_gets: registry.register(metric!(
1581                name: "mz_persist_blob_gets",
1582                help: "number of Blob::get calls for batch parts, over all shards",
1583            )),
1584            process_blob_sets: registry.register(metric!(
1585                name: "mz_persist_blob_sets",
1586                help: "number of Blob::set calls for batch parts, over all shards",
1587            )),
1588            process_encoded_diff_size: registry.register(metric!(
1589                name: "mz_persist_diff_size_bytes",
1590                help: "total encoded diff size, over all shards",
1591            )),
1592            process_unconsolidated_snapshot: registry.register(metric!(
1593                name: "mz_persist_unconsolidated_snapshot",
1594                help: "in snapshot_and_read, the number of times consolidating the raw data wasn't enough to produce consolidated output, over all shards",
1595            )),
1596            shards,
1597        }
1598    }
1599
1600    pub fn shard(&self, shard_id: &ShardId, name: &str) -> Arc<ShardMetrics> {
1601        let mut shards = self.shards.lock().expect("mutex poisoned");
1602        if let Some(shard) = shards.get(shard_id) {
1603            if let Some(shard) = shard.upgrade() {
1604                return Arc::clone(&shard);
1605            } else {
1606                assert!(shards.remove(shard_id).is_some());
1607            }
1608        }
1609        let shard = Arc::new(ShardMetrics::new(shard_id, name, self));
1610        assert!(
1611            shards
1612                .insert(shard_id.clone(), Arc::downgrade(&shard))
1613                .is_none()
1614        );
1615        shard
1616    }
1617
1618    /// Returns the shards that still have handles, and forgets the rest.
1619    ///
1620    /// Holds the `shards` lock, which `Self::shard` needs, so it does nothing
1621    /// but upgrade each entry.
1622    fn live_shards(
1623        shards: &Arc<Mutex<BTreeMap<ShardId, Weak<ShardMetrics>>>>,
1624    ) -> Vec<Arc<ShardMetrics>> {
1625        let mut shards = shards.lock().expect("mutex poisoned");
1626        let mut live = Vec::with_capacity(shards.len());
1627        shards.retain(|_, metrics| match metrics.upgrade() {
1628            Some(metrics) => {
1629                live.push(metrics);
1630                true
1631            }
1632            None => false,
1633        });
1634        live
1635    }
1636}
1637
1638/// A family that [`ShardsMetrics::new`] registers with one series per shard,
1639/// and the bounded aggregate that covers it.
1640struct PerShardFamily {
1641    name: &'static str,
1642    aggregate: ShardAggregate,
1643}
1644
1645enum ShardAggregate {
1646    /// Folded into `<name>_percentile` and `<name>_topk` at scrape time.
1647    Gauge(ShardGauge),
1648    /// Summed over shards into `mz_persist_batch_part_version_count`.
1649    VersionSum,
1650    /// Counted in full by the named process-level counter.
1651    ///
1652    /// NOTE: For `gc_finished`, `compaction_applied` and `cmd_succeeded` the
1653    /// process counter is kept equal to the sum over shards by a paired
1654    /// `.inc()` at each call site, not by construction like [`ShardCounter`].
1655    /// Add one without the other and the total drifts, with no test to catch it.
1656    Counter {
1657        // Only read by tests: that it names a registered counter, and that
1658        // every export mode keeps that counter.
1659        #[allow(dead_code)]
1660        total: &'static str,
1661    },
1662}
1663
1664struct ShardGauge {
1665    /// The registered help of the per-shard family, which the aggregate
1666    /// extends. `per_shard_families_match_registrations` checks they match.
1667    help: &'static str,
1668    /// How many of the largest shards `<name>_topk` names.
1669    top_k: usize,
1670    /// Whether to emit `<name>_sum`. See [`AggregatedFamily::sum`].
1671    sum: bool,
1672    get: fn(&ShardMetrics) -> u64,
1673}
1674
1675/// The default `ShardGauge::top_k`.
1676const SHARD_TOP_K: usize = 10;
1677
1678/// Every per-shard family. Must list exactly the families with a `shard`
1679/// label that `ShardsMetrics::new` registers.
1680const PER_SHARD_FAMILIES: &[PerShardFamily] = &[
1681    PerShardFamily {
1682        name: "mz_persist_shard_rollup_size_bytes",
1683        aggregate: ShardAggregate::Gauge(ShardGauge {
1684            help: "total encoded rollup size by shard",
1685            top_k: SHARD_TOP_K,
1686            sum: true,
1687            get: |m| m.latest_rollup_size.get(),
1688        }),
1689    },
1690    PerShardFamily {
1691        name: "mz_persist_shard_diff_size_bytes",
1692        aggregate: ShardAggregate::Counter {
1693            total: "mz_persist_diff_size_bytes",
1694        },
1695    },
1696    PerShardFamily {
1697        name: "mz_persist_shard_hollow_batch_count",
1698        aggregate: ShardAggregate::Gauge(ShardGauge {
1699            help: "count of hollow batches by shard",
1700            top_k: SHARD_TOP_K,
1701            sum: true,
1702            get: |m| m.hollow_batch_count.get(),
1703        }),
1704    },
1705    PerShardFamily {
1706        name: "mz_persist_shard_spine_batch_count",
1707        aggregate: ShardAggregate::Gauge(ShardGauge {
1708            help: "count of spine batches by shard",
1709            top_k: SHARD_TOP_K,
1710            sum: true,
1711            get: |m| m.spine_batch_count.get(),
1712        }),
1713    },
1714    PerShardFamily {
1715        name: "mz_persist_shard_batch_part_count",
1716        aggregate: ShardAggregate::Gauge(ShardGauge {
1717            help: "count of batch parts by shard",
1718            top_k: SHARD_TOP_K,
1719            sum: true,
1720            get: |m| m.batch_part_count.get(),
1721        }),
1722    },
1723    PerShardFamily {
1724        name: "mz_persist_shard_batch_part_version_count",
1725        aggregate: ShardAggregate::VersionSum,
1726    },
1727    PerShardFamily {
1728        name: "mz_persist_shard_update_count",
1729        aggregate: ShardAggregate::Gauge(ShardGauge {
1730            help: "count of updates by shard",
1731            top_k: SHARD_TOP_K,
1732            sum: true,
1733            get: |m| m.update_count.get(),
1734        }),
1735    },
1736    PerShardFamily {
1737        name: "mz_persist_shard_rollup_count",
1738        aggregate: ShardAggregate::Gauge(ShardGauge {
1739            help: "count of rollups by shard",
1740            top_k: SHARD_TOP_K,
1741            sum: true,
1742            get: |m| m.rollup_count.get(),
1743        }),
1744    },
1745    PerShardFamily {
1746        name: "mz_persist_shard_largest_batch_size",
1747        aggregate: ShardAggregate::Gauge(ShardGauge {
1748            help: "largest encoded batch size by shard",
1749            top_k: SHARD_TOP_K,
1750            sum: false,
1751            get: |m| m.largest_batch_size.get(),
1752        }),
1753    },
1754    PerShardFamily {
1755        name: "mz_persist_shard_seqnos_held",
1756        aggregate: ShardAggregate::Gauge(ShardGauge {
1757            help: "maximum count of gc-ineligible states by shard",
1758            top_k: SHARD_TOP_K,
1759            sum: true,
1760            get: |m| m.seqnos_held.get(),
1761        }),
1762    },
1763    PerShardFamily {
1764        name: "mz_persist_shard_seqnos_since_last_rollup",
1765        aggregate: ShardAggregate::Gauge(ShardGauge {
1766            help: "count of seqnos since last rollup",
1767            top_k: SHARD_TOP_K,
1768            sum: true,
1769            get: |m| m.seqnos_since_last_rollup.get(),
1770        }),
1771    },
1772    PerShardFamily {
1773        name: "mz_persist_shard_gc_seqno_held_parts",
1774        aggregate: ShardAggregate::Gauge(ShardGauge {
1775            help: "count of parts referenced by some live state but not the current state (ie. parts kept only to satisfy seqno holds) at GC time",
1776            top_k: SHARD_TOP_K,
1777            sum: true,
1778            get: |m| m.gc_seqno_held_parts.get(),
1779        }),
1780    },
1781    PerShardFamily {
1782        name: "mz_persist_shard_gc_live_diffs",
1783        aggregate: ShardAggregate::Gauge(ShardGauge {
1784            help: "the number of diffs (or, alternatively, the number of seqnos) present in consensus state at GC time",
1785            top_k: SHARD_TOP_K,
1786            sum: true,
1787            get: |m| m.gc_live_diffs.get(),
1788        }),
1789    },
1790    PerShardFamily {
1791        name: "mz_persist_shard_gc_finished",
1792        aggregate: ShardAggregate::Counter {
1793            total: "mz_persist_gc_finished",
1794        },
1795    },
1796    PerShardFamily {
1797        name: "mz_persist_shard_compaction_applied",
1798        aggregate: ShardAggregate::Counter {
1799            total: "mz_persist_compaction_applied",
1800        },
1801    },
1802    PerShardFamily {
1803        name: "mz_persist_shard_cmd_succeeded",
1804        aggregate: ShardAggregate::Counter {
1805            total: "mz_persist_cmd_succeeded_count",
1806        },
1807    },
1808    PerShardFamily {
1809        name: "mz_persist_shard_usage_current_state_batches_bytes",
1810        aggregate: ShardAggregate::Gauge(ShardGauge {
1811            help: "data in batches/parts referenced by current version of state",
1812            top_k: SHARD_TOP_K,
1813            sum: true,
1814            get: |m| m.usage_current_state_batches_bytes.get(),
1815        }),
1816    },
1817    PerShardFamily {
1818        name: "mz_persist_shard_usage_current_state_rollups_bytes",
1819        aggregate: ShardAggregate::Gauge(ShardGauge {
1820            help: "data in rollups referenced by current version of state",
1821            top_k: SHARD_TOP_K,
1822            sum: true,
1823            get: |m| m.usage_current_state_rollups_bytes.get(),
1824        }),
1825    },
1826    PerShardFamily {
1827        name: "mz_persist_shard_usage_referenced_not_current_state_bytes",
1828        aggregate: ShardAggregate::Gauge(ShardGauge {
1829            help: "data referenced only by a previous version of state",
1830            top_k: SHARD_TOP_K,
1831            sum: true,
1832            get: |m| m.usage_referenced_not_current_state_bytes.get(),
1833        }),
1834    },
1835    PerShardFamily {
1836        name: "mz_persist_shard_usage_not_leaked_not_referenced_bytes",
1837        aggregate: ShardAggregate::Gauge(ShardGauge {
1838            help: "data written by an active writer but not referenced by any version of state",
1839            top_k: SHARD_TOP_K,
1840            sum: true,
1841            get: |m| m.usage_not_leaked_not_referenced_bytes.get(),
1842        }),
1843    },
1844    PerShardFamily {
1845        name: "mz_persist_shard_usage_leaked_bytes",
1846        aggregate: ShardAggregate::Gauge(ShardGauge {
1847            help: "data reclaimable by a leaked blob detector",
1848            top_k: SHARD_TOP_K,
1849            sum: true,
1850            get: |m| m.usage_leaked_bytes.get(),
1851        }),
1852    },
1853    PerShardFamily {
1854        name: "mz_persist_shard_blob_gets",
1855        aggregate: ShardAggregate::Counter {
1856            total: "mz_persist_blob_gets",
1857        },
1858    },
1859    PerShardFamily {
1860        name: "mz_persist_shard_blob_sets",
1861        aggregate: ShardAggregate::Counter {
1862            total: "mz_persist_blob_sets",
1863        },
1864    },
1865    PerShardFamily {
1866        name: "mz_persist_shard_unconsolidated_snapshot",
1867        aggregate: ShardAggregate::Counter {
1868            total: "mz_persist_unconsolidated_snapshot",
1869        },
1870    },
1871    PerShardFamily {
1872        name: "mz_persist_shard_inline_part_count",
1873        aggregate: ShardAggregate::Gauge(ShardGauge {
1874            help: "count of parts inline in shard metadata",
1875            top_k: SHARD_TOP_K,
1876            sum: true,
1877            get: |m| m.inline_part_count.get(),
1878        }),
1879    },
1880    PerShardFamily {
1881        name: "mz_persist_shard_compact_batches",
1882        aggregate: ShardAggregate::Gauge(ShardGauge {
1883            help: "number of fully compact batches in the shard",
1884            top_k: SHARD_TOP_K,
1885            sum: true,
1886            get: |m| m.compact_batches.get(),
1887        }),
1888    },
1889    PerShardFamily {
1890        name: "mz_persist_shard_compacting_batches",
1891        aggregate: ShardAggregate::Gauge(ShardGauge {
1892            help: "number of batches in the shard with compactions in progress",
1893            top_k: SHARD_TOP_K,
1894            sum: true,
1895            get: |m| m.compacting_batches.get(),
1896        }),
1897    },
1898    PerShardFamily {
1899        name: "mz_persist_shard_noncompact_batches",
1900        aggregate: ShardAggregate::Gauge(ShardGauge {
1901            help: "number of batches in the shard that aren't compact and have no ongoing compaction",
1902            top_k: SHARD_TOP_K,
1903            sum: true,
1904            get: |m| m.noncompact_batches.get(),
1905        }),
1906    },
1907];
1908
1909/// The per-shard gauges and their names, in `PER_SHARD_FAMILIES` order.
1910fn shard_gauges() -> impl Iterator<Item = (&'static str, &'static ShardGauge)> {
1911    PER_SHARD_FAMILIES
1912        .iter()
1913        .filter_map(|f| match &f.aggregate {
1914            ShardAggregate::Gauge(g) => Some((f.name, g)),
1915            _ => None,
1916        })
1917}
1918
1919const SHARD_INFO_NAME: &str = "mz_persist_shard_info";
1920const BATCH_PART_VERSION_COUNT_NAME: &str = "mz_persist_batch_part_version_count";
1921const BATCH_PART_VERSION_COUNT_HELP: &str = "count of batch parts by version, over all shards";
1922
1923/// Identifies a shard in the `_topk` families, with the same labels as the
1924/// per-shard series.
1925#[derive(Debug)]
1926struct ShardKey(Arc<ShardMetrics>);
1927
1928impl AggregationKey for ShardKey {
1929    const LABEL_NAMES: &'static [&'static str] = &["shard", "name"];
1930
1931    fn label_values(&self) -> Vec<String> {
1932        vec![self.0.shard_id.to_string(), self.0.name.clone()]
1933    }
1934}
1935
1936/// Process-level metrics derived from the shards map, collected in a single
1937/// walk: the shard counts, percentiles, a top-K and (where meaningful) a sum
1938/// per per-shard gauge, the per-version batch part count summed over shards,
1939/// and, if [`SHARD_INFO_METRIC`] is set, `mz_persist_shard_info`.
1940///
1941/// All but `mz_persist_shard_info` have a cardinality independent of the
1942/// number of shards.
1943#[derive(Debug)]
1944struct ShardsAggregateMetrics {
1945    shards: Arc<Mutex<BTreeMap<ShardId, Weak<ShardMetrics>>>>,
1946    configs: Arc<ConfigSet>,
1947    count: GenericGauge<AtomicI64>,
1948    stale_count: GenericGauge<AtomicI64>,
1949    gauges: AggregatedFamilies<ShardKey>,
1950    /// The getters behind `gauges`, same order, so the per-shard walk reads a
1951    /// slice instead of re-deriving them per shard.
1952    gauge_gets: Vec<fn(&ShardMetrics) -> u64>,
1953    // Only used for `desc`. `collect` builds a fresh vec each scrape so that
1954    // versions no shard has anymore are not exported.
1955    batch_part_version_count: raw::UIntGaugeVec,
1956    // Only used for `desc`, like `batch_part_version_count`.
1957    shard_info: raw::UIntGaugeVec,
1958}
1959
1960impl ShardsAggregateMetrics {
1961    fn new(
1962        shards: Arc<Mutex<BTreeMap<ShardId, Weak<ShardMetrics>>>>,
1963        configs: Arc<ConfigSet>,
1964    ) -> Self {
1965        let gauges = shard_gauges()
1966            .map(|(name, g)| AggregatedFamily {
1967                name: name.to_string(),
1968                help: g.help.to_string(),
1969                sum: g.sum,
1970                top_k: Some(g.top_k),
1971            })
1972            .collect();
1973        ShardsAggregateMetrics {
1974            shards,
1975            configs,
1976            count: MakeCollector::make_collector(metric!(
1977                name: "mz_persist_shard_count",
1978                help: "count of all active shards on this process",
1979            )),
1980            stale_count: MakeCollector::make_collector(metric!(
1981                name: "mz_persist_stale_shard_count",
1982                help: "count of shards on this process whose persisted state version \
1983                       is behind this process's build version; per-process, so summing \
1984                       across processes counts (shard, process) pairs, not distinct shards",
1985            )),
1986            gauges: AggregatedFamilies::new(gauges),
1987            gauge_gets: shard_gauges().map(|(_, g)| g.get).collect(),
1988            batch_part_version_count: new_batch_part_version_count(),
1989            shard_info: new_shard_info(),
1990        }
1991    }
1992
1993    /// The bounded aggregates of the per-shard families over `live`.
1994    fn summaries(&self, live: Vec<Arc<ShardMetrics>>) -> Vec<MetricFamily> {
1995        let mut versions = BTreeMap::<String, u64>::new();
1996        let mut values = Vec::new();
1997        let mut snapshot = self.gauges.snapshot();
1998        for m in live {
1999            values.clear();
2000            values.extend(self.gauge_gets.iter().map(|get| get(&m)));
2001            let version_map = m
2002                .batch_part_version_map
2003                .lock()
2004                .expect("mutex should not be poisoned");
2005            for (version, metrics) in version_map.iter() {
2006                let n = metrics.batch_part_version_count.get();
2007                if n == 0 {
2008                    continue;
2009                }
2010                match versions.get_mut(version) {
2011                    Some(total) => *total += n,
2012                    None => {
2013                        versions.insert(version.clone(), n);
2014                    }
2015                }
2016            }
2017            drop(version_map);
2018            snapshot.push(ShardKey(m), &values);
2019        }
2020
2021        let mut families = snapshot.finish();
2022        let batch_part_version_count = new_batch_part_version_count();
2023        for (version, n) in versions {
2024            batch_part_version_count
2025                .with_label_values(&[version.as_str()])
2026                .set(n);
2027        }
2028        families.extend(batch_part_version_count.collect());
2029        families
2030    }
2031}
2032
2033/// Lists the shards, which the `_topk` families name only the largest of. One
2034/// series per shard, so unlike the summaries it grows with the shard count.
2035fn new_shard_info() -> raw::UIntGaugeVec {
2036    raw::UIntGaugeVec::new(
2037        PrometheusOpts::new(SHARD_INFO_NAME, "1 for each active shard on this process"),
2038        ShardKey::LABEL_NAMES,
2039    )
2040    .expect("valid gauge vec")
2041}
2042
2043fn new_batch_part_version_count() -> raw::UIntGaugeVec {
2044    raw::UIntGaugeVec::new(
2045        PrometheusOpts::new(BATCH_PART_VERSION_COUNT_NAME, BATCH_PART_VERSION_COUNT_HELP),
2046        &["version"],
2047    )
2048    .expect("valid gauge vec")
2049}
2050
2051impl Collector for ShardsAggregateMetrics {
2052    fn desc(&self) -> Vec<&Desc> {
2053        let mut descs = self.count.desc();
2054        descs.extend(self.stale_count.desc());
2055        descs.extend(self.gauges.descs());
2056        descs.extend(self.batch_part_version_count.desc());
2057        descs.extend(self.shard_info.desc());
2058        descs
2059    }
2060
2061    fn collect(&self) -> Vec<MetricFamily> {
2062        let live = ShardsMetrics::live_shards(&self.shards);
2063        let mut count = 0;
2064        let mut stale_count = 0;
2065        for m in &live {
2066            count += 1;
2067            if m.stale.load(Ordering::Relaxed) {
2068                stale_count += 1;
2069            }
2070        }
2071        self.count.set(count);
2072        self.stale_count.set(stale_count);
2073
2074        let mut families = Vec::new();
2075        if SHARD_INFO_METRIC.get(&self.configs) {
2076            let shard_info = new_shard_info();
2077            for m in &live {
2078                shard_info
2079                    .with_label_values(&[m.shard_id.to_string(), m.name.clone()])
2080                    .set(1);
2081            }
2082            families.extend(shard_info.collect());
2083        }
2084        if ShardMetricsExport::get(&self.configs).summary() {
2085            families.extend(self.summaries(live));
2086        }
2087        families.extend(self.count.collect());
2088        families.extend(self.stale_count.collect());
2089        families
2090    }
2091}
2092
2093#[derive(Debug)]
2094pub struct ShardMetrics {
2095    pub shard_id: ShardId,
2096    pub name: String,
2097    pub largest_batch_size: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2098    pub latest_rollup_size: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2099    pub encoded_diff_size: ShardCounter,
2100    pub hollow_batch_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2101    pub spine_batch_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2102    pub batch_part_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2103    batch_part_version_count: mz_ore::metrics::UIntGaugeVec,
2104    batch_part_version_map: Mutex<BTreeMap<String, BatchPartVersionMetrics>>,
2105    pub update_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2106    pub rollup_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2107    pub seqnos_held: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2108    pub seqnos_since_last_rollup: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2109    pub gc_seqno_held_parts: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2110    pub gc_live_diffs: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2111    pub usage_current_state_batches_bytes: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2112    pub usage_current_state_rollups_bytes: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2113    pub usage_referenced_not_current_state_bytes: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2114    pub usage_not_leaked_not_referenced_bytes: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2115    pub usage_leaked_bytes: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2116    pub gc_finished: DeleteOnDropCounter<AtomicU64, Vec<String>>,
2117    pub compaction_applied: DeleteOnDropCounter<AtomicU64, Vec<String>>,
2118    pub cmd_succeeded: DeleteOnDropCounter<AtomicU64, Vec<String>>,
2119    pub blob_gets: ShardCounter,
2120    pub blob_sets: ShardCounter,
2121    pub unconsolidated_snapshot: ShardCounter,
2122    pub inline_part_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2123    pub compact_batches: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2124    pub compacting_batches: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2125    pub noncompact_batches: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2126    // Not a registered per-shard series: this feeds the process-level
2127    // `mz_persist_stale_shard_count` computed gauge, which sums it across shards
2128    // instead of exporting one series per shard.
2129    pub stale: AtomicBool,
2130}
2131
2132impl ShardMetrics {
2133    pub fn new(shard_id: &ShardId, name: &str, shards_metrics: &ShardsMetrics) -> Self {
2134        let shard = shard_id.to_string();
2135        ShardMetrics {
2136            shard_id: *shard_id,
2137            name: name.to_string(),
2138            latest_rollup_size: shards_metrics
2139                .encoded_rollup_size
2140                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2141            encoded_diff_size: ShardCounter {
2142                shard: shards_metrics
2143                    .encoded_diff_size
2144                    .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2145                process: shards_metrics.process_encoded_diff_size.clone(),
2146            },
2147            hollow_batch_count: shards_metrics
2148                .hollow_batch_count
2149                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2150            spine_batch_count: shards_metrics
2151                .spine_batch_count
2152                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2153            batch_part_count: shards_metrics
2154                .batch_part_count
2155                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2156            batch_part_version_count: shards_metrics.batch_part_version_count.clone(),
2157            batch_part_version_map: Mutex::new(BTreeMap::new()),
2158            update_count: shards_metrics
2159                .update_count
2160                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2161            rollup_count: shards_metrics
2162                .rollup_count
2163                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2164            largest_batch_size: shards_metrics
2165                .largest_batch_size
2166                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2167            seqnos_held: shards_metrics
2168                .seqnos_held
2169                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2170            seqnos_since_last_rollup: shards_metrics
2171                .seqnos_since_last_rollup
2172                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2173            gc_seqno_held_parts: shards_metrics
2174                .gc_seqno_held_parts
2175                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2176            gc_live_diffs: shards_metrics
2177                .gc_live_diffs
2178                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2179            gc_finished: shards_metrics
2180                .gc_finished
2181                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2182            compaction_applied: shards_metrics
2183                .compaction_applied
2184                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2185            cmd_succeeded: shards_metrics
2186                .cmd_succeeded
2187                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2188            usage_current_state_batches_bytes: shards_metrics
2189                .usage_current_state_batches_bytes
2190                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2191            usage_current_state_rollups_bytes: shards_metrics
2192                .usage_current_state_rollups_bytes
2193                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2194            usage_referenced_not_current_state_bytes: shards_metrics
2195                .usage_referenced_not_current_state_bytes
2196                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2197            usage_not_leaked_not_referenced_bytes: shards_metrics
2198                .usage_not_leaked_not_referenced_bytes
2199                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2200            usage_leaked_bytes: shards_metrics
2201                .usage_leaked_bytes
2202                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2203            blob_gets: ShardCounter {
2204                shard: shards_metrics
2205                    .blob_gets
2206                    .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2207                process: shards_metrics.process_blob_gets.clone(),
2208            },
2209            blob_sets: ShardCounter {
2210                shard: shards_metrics
2211                    .blob_sets
2212                    .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2213                process: shards_metrics.process_blob_sets.clone(),
2214            },
2215            unconsolidated_snapshot: ShardCounter {
2216                shard: shards_metrics
2217                    .unconsolidated_snapshot
2218                    .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2219                process: shards_metrics.process_unconsolidated_snapshot.clone(),
2220            },
2221            inline_part_count: shards_metrics
2222                .inline_part_count
2223                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2224            compact_batches: shards_metrics
2225                .compact_batches
2226                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2227            compacting_batches: shards_metrics
2228                .compacting_batches
2229                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2230            noncompact_batches: shards_metrics
2231                .noncompact_batches
2232                .get_delete_on_drop_metric(vec![shard, name.to_string()]),
2233            stale: AtomicBool::new(false),
2234        }
2235    }
2236
2237    pub(crate) fn set_batch_part_versions<'a>(
2238        &self,
2239        batch_parts_by_version: impl Iterator<Item = &'a str>,
2240    ) {
2241        let mut map = self
2242            .batch_part_version_map
2243            .lock()
2244            .expect("mutex should not be poisoned");
2245        // NB: It's a bit sus that the below assumes that no one else is
2246        // concurrently modifying the atomics in the gauges, but we're holding
2247        // the mutex this whole time, so it should be true.
2248
2249        // We want to do this in a way that avoids allocating (e.g. summing up a
2250        // map). First reset everything.
2251        for x in map.values() {
2252            x.batch_part_version_count.set(0);
2253        }
2254
2255        // Then go through the iterator, creating new entries as necessary and
2256        // adding.
2257        for key in batch_parts_by_version {
2258            if !map.contains_key(key) {
2259                map.insert(
2260                    key.to_owned(),
2261                    BatchPartVersionMetrics {
2262                        batch_part_version_count: self
2263                            .batch_part_version_count
2264                            .get_delete_on_drop_metric(vec![
2265                                self.shard_id.to_string(),
2266                                self.name.clone(),
2267                                key.to_owned(),
2268                            ]),
2269                    },
2270                );
2271            }
2272            let value = map.get(key).expect("inserted above");
2273            value.batch_part_version_count.inc();
2274        }
2275    }
2276}
2277
2278/// A per-shard counter that also bumps a process-level total, so the total
2279/// stays monotonic when shards are dropped.
2280#[derive(Debug)]
2281pub struct ShardCounter {
2282    shard: DeleteOnDropCounter<AtomicU64, Vec<String>>,
2283    process: IntCounter,
2284}
2285
2286impl ShardCounter {
2287    pub fn inc(&self) {
2288        self.inc_by(1);
2289    }
2290
2291    pub fn inc_by(&self, v: u64) {
2292        self.shard.inc_by(v);
2293        self.process.inc_by(v);
2294    }
2295}
2296
2297#[derive(Debug)]
2298pub struct BatchPartVersionMetrics {
2299    pub batch_part_version_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2300}
2301
2302/// Metrics recorded by audits of persist usage
2303#[derive(Debug)]
2304pub struct UsageAuditMetrics {
2305    /// Size of all batch parts stored in Blob
2306    pub blob_batch_part_bytes: UIntGauge,
2307    /// Count of batch parts stored in Blob
2308    pub blob_batch_part_count: UIntGauge,
2309    /// Size of all state rollups stored in Blob
2310    pub blob_rollup_bytes: UIntGauge,
2311    /// Count of state rollups stored in Blob
2312    pub blob_rollup_count: UIntGauge,
2313    /// Size of Blob
2314    pub blob_bytes: UIntGauge,
2315    /// Count of all blobs
2316    pub blob_count: UIntGauge,
2317    /// Time spent fetching blob metadata
2318    pub step_blob_metadata: Counter,
2319    /// Time spent fetching state versions
2320    pub step_state: Counter,
2321    /// Time spent doing math
2322    pub step_math: Counter,
2323}
2324
2325impl UsageAuditMetrics {
2326    fn new(registry: &MetricsRegistry) -> Self {
2327        let step_timings: CounterVec = registry.register(metric!(
2328                name: "mz_persist_audit_step_seconds",
2329                help: "time spent on individual steps of audit",
2330                var_labels: ["step"],
2331        ));
2332        UsageAuditMetrics {
2333            blob_batch_part_bytes: registry.register(metric!(
2334                name: "mz_persist_audit_blob_batch_part_bytes",
2335                help: "total size of batch parts in blob",
2336            )),
2337            blob_batch_part_count: registry.register(metric!(
2338                name: "mz_persist_audit_blob_batch_part_count",
2339                help: "count of batch parts in blob",
2340            )),
2341            blob_rollup_bytes: registry.register(metric!(
2342                name: "mz_persist_audit_blob_rollup_bytes",
2343                help: "total size of state rollups stored in blob",
2344            )),
2345            blob_rollup_count: registry.register(metric!(
2346                name: "mz_persist_audit_blob_rollup_count",
2347                help: "count of all state rollups in blob",
2348            )),
2349            blob_bytes: registry.register(metric!(
2350                name: "mz_persist_audit_blob_bytes",
2351                help: "total size of blob",
2352            )),
2353            blob_count: registry.register(metric!(
2354                name: "mz_persist_audit_blob_count",
2355                help: "count of all blobs",
2356            )),
2357            step_blob_metadata: step_timings.with_label_values(&["blob_metadata"]),
2358            step_state: step_timings.with_label_values(&["state"]),
2359            step_math: step_timings.with_label_values(&["math"]),
2360        }
2361    }
2362}
2363
2364/// Represents a change in a number of updates kept in a data structure
2365/// (e.g., a buffer length or capacity change).
2366#[derive(Debug)]
2367pub enum UpdateDelta {
2368    /// A negative delta in the number of updates.
2369    Negative(u64),
2370    /// A non-negative delta in the number of updates.
2371    NonNegative(u64),
2372}
2373
2374impl UpdateDelta {
2375    /// Creates a new `UpdateDelta` from the difference between a new value
2376    /// for a number of updates and the corresponding old value.
2377    pub fn new(new: usize, old: usize) -> Self {
2378        if new < old {
2379            UpdateDelta::Negative(CastFrom::cast_from(old - new))
2380        } else {
2381            UpdateDelta::NonNegative(CastFrom::cast_from(new - old))
2382        }
2383    }
2384}
2385
2386/// Metrics for the persist sink. (While this lies slightly outside the usual
2387/// abstraction boundary of the client, it's convenient to manage them together.
2388#[derive(Debug, Clone)]
2389pub struct SinkMetrics {
2390    /// Cumulative record insertions made to the correction buffer across workers
2391    correction_insertions_total: IntCounter,
2392    /// Cumulative record deletions made to the correction buffer across workers
2393    correction_deletions_total: IntCounter,
2394    /// Cumulative capacity increases made to the correction buffer across workers
2395    correction_capacity_increases_total: IntCounter,
2396    /// Cumulative capacity decreases made to the correction buffer across workers
2397    correction_capacity_decreases_total: IntCounter,
2398    /// Maximum length observed for any one correction buffer per worker
2399    correction_max_per_sink_worker_len_updates: raw::UIntGaugeVec,
2400    /// Maximum capacity observed for any one correction buffer per worker
2401    correction_max_per_sink_worker_capacity_updates: raw::UIntGaugeVec,
2402}
2403
2404impl SinkMetrics {
2405    fn new(registry: &MetricsRegistry) -> Self {
2406        SinkMetrics {
2407            correction_insertions_total: registry.register(metric!(
2408                name: "mz_persist_sink_correction_insertions_total",
2409                help: "The cumulative insertions observed on the correction buffer across workers and persist sinks.",
2410            )),
2411            correction_deletions_total: registry.register(metric!(
2412                name: "mz_persist_sink_correction_deletions_total",
2413                help: "The cumulative deletions observed on the correction buffer across workers and persist sinks.",
2414            )),
2415            correction_capacity_increases_total: registry.register(metric!(
2416                name: "mz_persist_sink_correction_capacity_increases_total",
2417                help: "The cumulative capacity increases observed on the correction buffer across workers and persist sinks.",
2418            )),
2419            correction_capacity_decreases_total: registry.register(metric!(
2420                name: "mz_persist_sink_correction_capacity_decreases_total",
2421                help: "The cumulative capacity decreases observed on the correction buffer across workers and persist sinks.",
2422            )),
2423            correction_max_per_sink_worker_len_updates: registry.register(metric!(
2424                name: "mz_persist_sink_correction_max_per_sink_worker_len_updates",
2425                help: "The maximum length observed for the correction buffer of any single persist sink per worker.",
2426                var_labels: ["worker_id"],
2427            )),
2428            correction_max_per_sink_worker_capacity_updates: registry.register(metric!(
2429                name: "mz_persist_sink_correction_max_per_sink_worker_capacity_updates",
2430                help: "The maximum capacity observed for the correction buffer of any single persist sink per worker.",
2431                var_labels: ["worker_id"],
2432            )),
2433        }
2434    }
2435
2436    /// Obtains a `SinkWorkerMetrics` instance, which allows for metric reporting
2437    /// from a specific `persist_sink` instance for a given worker. The reports will
2438    /// update metrics shared across workers, but provide per-worker contributions
2439    /// to them.
2440    pub fn for_worker(&self, worker_id: usize) -> SinkWorkerMetrics {
2441        let worker = worker_id.to_string();
2442        let correction_max_per_sink_worker_len_updates = self
2443            .correction_max_per_sink_worker_len_updates
2444            .with_label_values(&[&worker]);
2445        let correction_max_per_sink_worker_capacity_updates = self
2446            .correction_max_per_sink_worker_capacity_updates
2447            .with_label_values(&[&worker]);
2448        SinkWorkerMetrics {
2449            correction_max_per_sink_worker_len_updates,
2450            correction_max_per_sink_worker_capacity_updates,
2451        }
2452    }
2453
2454    /// Reports updates to the length and capacity of the correction buffer in the
2455    /// `write_batches` operator of a `persist_sink`.
2456    ///
2457    /// This method updates monotonic metrics based on the deltas and thus can be
2458    /// called across workers and instances of `persist_sink`.
2459    pub fn report_correction_update_deltas(
2460        &self,
2461        correction_len_delta: UpdateDelta,
2462        correction_cap_delta: UpdateDelta,
2463    ) {
2464        // Report insertions or deletions.
2465        match correction_len_delta {
2466            UpdateDelta::NonNegative(delta) => {
2467                if delta > 0 {
2468                    self.correction_insertions_total.inc_by(delta)
2469                }
2470            }
2471            UpdateDelta::Negative(delta) => self.correction_deletions_total.inc_by(delta),
2472        }
2473        // Report capacity increases or decreases.
2474        match correction_cap_delta {
2475            UpdateDelta::NonNegative(delta) => {
2476                if delta > 0 {
2477                    self.correction_capacity_increases_total.inc_by(delta)
2478                }
2479            }
2480            UpdateDelta::Negative(delta) => self.correction_capacity_decreases_total.inc_by(delta),
2481        }
2482    }
2483}
2484
2485/// Metrics for the persist sink that are labeled per-worker.
2486#[derive(Clone, Debug)]
2487pub struct SinkWorkerMetrics {
2488    correction_max_per_sink_worker_len_updates: UIntGauge,
2489    correction_max_per_sink_worker_capacity_updates: UIntGauge,
2490}
2491
2492impl SinkWorkerMetrics {
2493    /// Reports the length and capacity of the correction buffer in the `write_batches`
2494    /// operator of `persist_sink`.
2495    ///
2496    /// This method is used to update metrics that are kept per worker.
2497    pub fn report_correction_update_totals(&self, correction_len: usize, correction_cap: usize) {
2498        // Maintain per-worker peaks.
2499        let correction_len = CastFrom::cast_from(correction_len);
2500        if correction_len > self.correction_max_per_sink_worker_len_updates.get() {
2501            self.correction_max_per_sink_worker_len_updates
2502                .set(correction_len);
2503        }
2504        let correction_cap = CastFrom::cast_from(correction_cap);
2505        if correction_cap > self.correction_max_per_sink_worker_capacity_updates.get() {
2506            self.correction_max_per_sink_worker_capacity_updates
2507                .set(correction_cap);
2508        }
2509    }
2510}
2511
2512/// Metrics for the `persist_source` backpressure operator, summed over every
2513/// instance of the operator in the process. Like [SinkMetrics], these belong
2514/// to a dataflow operator rather than the client, but the client owns the only
2515/// registry the operator can reach.
2516#[derive(Debug)]
2517pub struct BackpressureMetrics {
2518    /// Bytes emitted by backpressure operators.
2519    pub emitted_bytes: IntCounter,
2520    /// Sum over live operator instances of the inflight bytes each one most
2521    /// recently stalled on. Instances contribute deltas, so the sum stays exact
2522    /// as operators start and stop.
2523    pub last_backpressured_bytes: UIntGauge,
2524    /// Bytes retired by processing downstream of backpressure operators.
2525    pub retired_bytes: IntCounter,
2526}
2527
2528impl BackpressureMetrics {
2529    fn new(registry: &MetricsRegistry) -> Self {
2530        BackpressureMetrics {
2531            emitted_bytes: registry.register(metric!(
2532                name: "mz_persist_backpressure_emitted_bytes",
2533                help: "bytes emitted by backpressure operators",
2534            )),
2535            last_backpressured_bytes: registry.register(metric!(
2536                name: "mz_persist_backpressure_last_backpressured_bytes",
2537                help: "sum over backpressure operators of the inflight bytes each last \
2538                    stalled on; not comparable to retired bytes, but nonzero growth \
2539                    indicates backpressure is happening",
2540            )),
2541            retired_bytes: registry.register(metric!(
2542                name: "mz_persist_backpressure_retired_bytes",
2543                help: "bytes retired by processing downstream of backpressure operators",
2544            )),
2545        }
2546    }
2547}
2548
2549/// A minimal set of metrics imported into honeycomb for alerting.
2550#[derive(Debug)]
2551pub struct AlertsMetrics {
2552    pub(crate) blob_failures: IntCounter,
2553    pub(crate) consensus_failures: IntCounter,
2554}
2555
2556impl AlertsMetrics {
2557    fn new(registry: &MetricsRegistry) -> Self {
2558        AlertsMetrics {
2559            blob_failures: registry.register(metric!(
2560                name: "mz_persist_blob_failures",
2561                help: "count of all blob operation failures",
2562                const_labels: {"honeycomb" => "import"},
2563            )),
2564            consensus_failures: registry.register(metric!(
2565                name: "mz_persist_consensus_failures",
2566                help: "count of determinate consensus operation failures",
2567                const_labels: {"honeycomb" => "import"},
2568            )),
2569        }
2570    }
2571}
2572
2573/// Metrics for the PubSubServer implementation.
2574#[derive(Debug)]
2575pub struct PubSubServerMetrics {
2576    pub(crate) active_connections: UIntGauge,
2577    pub(crate) broadcasted_diff_count: IntCounter,
2578    pub(crate) broadcasted_diff_bytes: IntCounter,
2579    pub(crate) broadcasted_diff_dropped_channel_full: IntCounter,
2580
2581    pub(crate) push_seconds: Counter,
2582    pub(crate) subscribe_seconds: Counter,
2583    pub(crate) unsubscribe_seconds: Counter,
2584    pub(crate) connection_cleanup_seconds: Counter,
2585
2586    pub(crate) push_call_count: IntCounter,
2587    pub(crate) subscribe_call_count: IntCounter,
2588    pub(crate) unsubscribe_call_count: IntCounter,
2589}
2590
2591impl PubSubServerMetrics {
2592    pub(crate) fn new(registry: &MetricsRegistry) -> Self {
2593        let op_timings: CounterVec = registry.register(metric!(
2594                name: "mz_persist_pubsub_server_operation_seconds",
2595                help: "time spent in pubsub server performing each operation",
2596                var_labels: ["op"],
2597        ));
2598        let call_count: IntCounterVec = registry.register(metric!(
2599                name: "mz_persist_pubsub_server_call_count",
2600                help: "count of each pubsub server message received",
2601                var_labels: ["call"],
2602        ));
2603
2604        Self {
2605            active_connections: registry.register(metric!(
2606                    name: "mz_persist_pubsub_server_active_connections",
2607                    help: "number of active connections to server",
2608            )),
2609            broadcasted_diff_count: registry.register(metric!(
2610                    name: "mz_persist_pubsub_server_broadcasted_diff_count",
2611                    help: "count of total broadcast diff messages sent",
2612            )),
2613            broadcasted_diff_bytes: registry.register(metric!(
2614                    name: "mz_persist_pubsub_server_broadcasted_diff_bytes",
2615                    help: "count of total broadcast diff bytes sent",
2616            )),
2617            broadcasted_diff_dropped_channel_full: registry.register(metric!(
2618                    name: "mz_persist_pubsub_server_broadcasted_diff_dropped_channel_full",
2619                    help: "count of diffs dropped due to full connection channel",
2620            )),
2621
2622            push_seconds: op_timings.with_label_values(&["push"]),
2623            subscribe_seconds: op_timings.with_label_values(&["subscribe"]),
2624            unsubscribe_seconds: op_timings.with_label_values(&["unsubscribe"]),
2625            connection_cleanup_seconds: op_timings.with_label_values(&["cleanup"]),
2626
2627            push_call_count: call_count.with_label_values(&["push"]),
2628            subscribe_call_count: call_count.with_label_values(&["subscribe"]),
2629            unsubscribe_call_count: call_count.with_label_values(&["unsubscribe"]),
2630        }
2631    }
2632}
2633
2634/// Metrics for the PubSubClient implementation.
2635#[derive(Debug)]
2636pub struct PubSubClientMetrics {
2637    pub sender: PubSubClientSenderMetrics,
2638    pub receiver: PubSubClientReceiverMetrics,
2639    pub grpc_connection: PubSubGrpcClientConnectionMetrics,
2640}
2641
2642impl PubSubClientMetrics {
2643    fn new(registry: &MetricsRegistry) -> Self {
2644        PubSubClientMetrics {
2645            sender: PubSubClientSenderMetrics::new(registry),
2646            receiver: PubSubClientReceiverMetrics::new(registry),
2647            grpc_connection: PubSubGrpcClientConnectionMetrics::new(registry),
2648        }
2649    }
2650}
2651
2652#[derive(Debug)]
2653pub struct PubSubGrpcClientConnectionMetrics {
2654    pub(crate) connected: UIntGauge,
2655    pub(crate) connection_established_count: IntCounter,
2656    pub(crate) connect_call_attempt_count: IntCounter,
2657    pub(crate) broadcast_recv_lagged_count: IntCounter,
2658    pub(crate) grpc_error_count: IntCounter,
2659}
2660
2661impl PubSubGrpcClientConnectionMetrics {
2662    fn new(registry: &MetricsRegistry) -> Self {
2663        Self {
2664            connected: registry.register(metric!(
2665                    name: "mz_persist_pubsub_client_grpc_connected",
2666                    help: "whether the grpc client is currently connected",
2667            )),
2668            connection_established_count: registry.register(metric!(
2669                    name: "mz_persist_pubsub_client_grpc_connection_established_count",
2670                    help: "count of grpc connection establishments to pubsub server",
2671            )),
2672            connect_call_attempt_count: registry.register(metric!(
2673                    name: "mz_persist_pubsub_client_grpc_connect_call_attempt_count",
2674                    help: "count of connection call attempts (including retries) to pubsub server",
2675            )),
2676            broadcast_recv_lagged_count: registry.register(metric!(
2677                    name: "mz_persist_pubsub_client_grpc_broadcast_recv_lagged_count",
2678                    help: "times a message was missed by broadcast receiver due to lag",
2679            )),
2680            grpc_error_count: registry.register(metric!(
2681                    name: "mz_persist_pubsub_client_grpc_error_count",
2682                    help: "count of grpc errors received",
2683            )),
2684        }
2685    }
2686}
2687
2688#[derive(Clone, Debug)]
2689pub struct PubSubClientReceiverMetrics {
2690    pub(crate) push_received: IntCounter,
2691    pub(crate) unknown_message_received: IntCounter,
2692    pub(crate) approx_diff_latency_seconds: Histogram,
2693
2694    pub(crate) state_pushed_diff_fast_path: IntCounter,
2695    pub(crate) state_pushed_diff_slow_path_succeeded: IntCounter,
2696    pub(crate) state_pushed_diff_slow_path_failed: IntCounter,
2697
2698    pub(crate) diff_applied: IntCounter,
2699    pub(crate) diff_not_applied_stale: IntCounter,
2700    pub(crate) diff_not_applied_out_of_order: IntCounter,
2701}
2702
2703impl PubSubClientReceiverMetrics {
2704    fn new(registry: &MetricsRegistry) -> Self {
2705        let call_received: IntCounterVec = registry.register(metric!(
2706                name: "mz_persist_pubsub_client_call_received",
2707                help: "times a pubsub client call was received",
2708                var_labels: ["call"],
2709        ));
2710
2711        Self {
2712            push_received: call_received.with_label_values(&["push"]),
2713            unknown_message_received: call_received.with_label_values(&["unknown"]),
2714            approx_diff_latency_seconds: registry.register(metric!(
2715                name: "mz_persist_pubsub_client_approx_diff_apply_latency_seconds",
2716                help: "histogram of (approximate) latency between sending a diff and applying it",
2717                buckets: prometheus::exponential_buckets(0.001, 2.0, 13).expect("buckets"),
2718            )),
2719
2720            state_pushed_diff_fast_path: registry.register(metric!(
2721                name: "mz_persist_pubsub_client_receiver_state_push_diff_fast_path",
2722                help: "count fast-path state push_diff calls",
2723            )),
2724            state_pushed_diff_slow_path_succeeded: registry.register(metric!(
2725                name: "mz_persist_pubsub_client_receiver_state_push_diff_slow_path_succeeded",
2726                help: "count of successful slow-path state push_diff calls",
2727            )),
2728            state_pushed_diff_slow_path_failed: registry.register(metric!(
2729                name: "mz_persist_pubsub_client_receiver_state_push_diff_slow_path_failed",
2730                help: "count of unsuccessful slow-path state push_diff calls",
2731            )),
2732            diff_applied: registry.register(metric!(
2733                name: "mz_persist_pubsub_client_receiver_diff_applied",
2734                help: "number of diffs received via pubsub that applied",
2735            )),
2736            diff_not_applied_stale: registry.register(metric!(
2737                name: "mz_persist_pubsub_client_receiver_diff_not_applied_stale",
2738                help: "number of diffs received via pubsub that did not apply due to staleness",
2739            )),
2740            diff_not_applied_out_of_order: registry.register(metric!(
2741                name: "mz_persist_pubsub_client_receiver_diff_not_applied_out_of_order",
2742                help: "number of diffs received via pubsub that did not apply due to out-of-order delivery",
2743            )),
2744        }
2745    }
2746}
2747
2748#[derive(Debug)]
2749pub struct PubSubClientSenderMetrics {
2750    pub push: PubSubClientCallMetrics,
2751    pub subscribe: PubSubClientCallMetrics,
2752    pub unsubscribe: PubSubClientCallMetrics,
2753}
2754
2755#[derive(Debug)]
2756pub struct PubSubClientCallMetrics {
2757    pub(crate) succeeded: IntCounter,
2758    pub(crate) bytes_sent: IntCounter,
2759    pub(crate) failed: IntCounter,
2760}
2761
2762impl PubSubClientSenderMetrics {
2763    fn new(registry: &MetricsRegistry) -> Self {
2764        let call_bytes_sent: IntCounterVec = registry.register(metric!(
2765                name: "mz_persist_pubsub_client_call_bytes_sent",
2766                help: "number of bytes sent for a given pubsub client call",
2767                var_labels: ["call"],
2768        ));
2769        let call_succeeded: IntCounterVec = registry.register(metric!(
2770                name: "mz_persist_pubsub_client_call_succeeded",
2771                help: "times a pubsub client call succeeded",
2772                var_labels: ["call"],
2773        ));
2774        let call_failed: IntCounterVec = registry.register(metric!(
2775                name: "mz_persist_pubsub_client_call_failed",
2776                help: "times a pubsub client call failed",
2777                var_labels: ["call"],
2778        ));
2779
2780        Self {
2781            push: PubSubClientCallMetrics {
2782                succeeded: call_succeeded.with_label_values(&["push"]),
2783                failed: call_failed.with_label_values(&["push"]),
2784                bytes_sent: call_bytes_sent.with_label_values(&["push"]),
2785            },
2786            subscribe: PubSubClientCallMetrics {
2787                succeeded: call_succeeded.with_label_values(&["subscribe"]),
2788                failed: call_failed.with_label_values(&["subscribe"]),
2789                bytes_sent: call_bytes_sent.with_label_values(&["subscribe"]),
2790            },
2791            unsubscribe: PubSubClientCallMetrics {
2792                succeeded: call_succeeded.with_label_values(&["unsubscribe"]),
2793                failed: call_failed.with_label_values(&["unsubscribe"]),
2794                bytes_sent: call_bytes_sent.with_label_values(&["unsubscribe"]),
2795            },
2796        }
2797    }
2798}
2799
2800#[derive(Debug)]
2801pub struct LocksMetrics {
2802    pub(crate) applier_read_cacheable: LockMetrics,
2803    pub(crate) applier_read_noncacheable: LockMetrics,
2804    pub(crate) applier_write: LockMetrics,
2805    pub(crate) watch: LockMetrics,
2806}
2807
2808#[derive(Debug, Clone)]
2809pub struct LockMetrics {
2810    pub(crate) acquire_count: IntCounter,
2811    pub(crate) blocking_acquire_count: IntCounter,
2812    pub(crate) blocking_seconds: Counter,
2813}
2814
2815#[derive(Debug)]
2816pub struct WatchMetrics {
2817    pub(crate) wait_woken_via_watch: IntCounter,
2818    pub(crate) wait_woken_via_sleep: IntCounter,
2819    pub(crate) wait_resolved_via_watch: IntCounter,
2820    pub(crate) wait_resolved_via_sleep: IntCounter,
2821    pub(crate) notify_sent: IntCounter,
2822    pub(crate) notify_upper_sent: IntCounter,
2823    pub(crate) notify_noop: IntCounter,
2824    pub(crate) notify_recv: IntCounter,
2825    pub(crate) notify_lagged: IntCounter,
2826    pub(crate) notify_wait_started: IntCounter,
2827    pub(crate) notify_wait_finished: IntCounter,
2828}
2829
2830impl WatchMetrics {
2831    fn new(registry: &MetricsRegistry) -> Self {
2832        WatchMetrics {
2833            wait_woken_via_watch: registry.register(metric!(
2834                name: "mz_persist_wait_woken_via_watch",
2835                help: "count of wait-for-uppers wakes via watch notify",
2836            )),
2837            wait_woken_via_sleep: registry.register(metric!(
2838                name: "mz_persist_wait_woken_via_sleep",
2839                help: "count of wait-for-uppers wakes via sleep",
2840            )),
2841            wait_resolved_via_watch: registry.register(metric!(
2842                name: "mz_persist_wait_resolved_via_watch",
2843                help: "count of wait-for-uppers resolved via watch notify",
2844            )),
2845            wait_resolved_via_sleep: registry.register(metric!(
2846                name: "mz_persist_wait_resolved_via_sleep",
2847                help: "count of wait-for-uppers resolved via sleep",
2848            )),
2849            notify_sent: registry.register(metric!(
2850                name: "mz_persist_watch_notify_sent",
2851                help: "count of watch notifications sent to a non-empty broadcast channel",
2852            )),
2853            notify_upper_sent: registry.register(metric!(
2854                name: "mz_persist_watch_notify_upper_sent",
2855                help: "count of strict shard upper advances signaled to upper waiters",
2856            )),
2857            notify_noop: registry.register(metric!(
2858                name: "mz_persist_watch_notify_noop",
2859                help: "count of watch notifications sent to an broadcast channel",
2860            )),
2861            notify_recv: registry.register(metric!(
2862                name: "mz_persist_watch_notify_recv",
2863                help: "count of watch notifications received from the broadcast channel",
2864            )),
2865            notify_lagged: registry.register(metric!(
2866                name: "mz_persist_watch_notify_lagged",
2867                help: "count of lagged events in the watch notification broadcast channel",
2868            )),
2869            notify_wait_started: registry.register(metric!(
2870                name: "mz_persist_watch_notify_wait_started",
2871                help: "count of watch wait calls started",
2872            )),
2873            notify_wait_finished: registry.register(metric!(
2874                name: "mz_persist_watch_notify_wait_finished",
2875                help: "count of watch wait calls resolved",
2876            )),
2877        }
2878    }
2879}
2880
2881#[derive(Debug)]
2882pub struct PushdownMetrics {
2883    pub(crate) parts_filtered_count: IntCounter,
2884    pub(crate) parts_filtered_bytes: IntCounter,
2885    pub(crate) parts_fetched_count: IntCounter,
2886    pub(crate) parts_fetched_bytes: IntCounter,
2887    pub(crate) parts_audited_count: IntCounter,
2888    pub(crate) parts_audited_bytes: IntCounter,
2889    pub(crate) parts_inline_count: IntCounter,
2890    pub(crate) parts_inline_bytes: IntCounter,
2891    pub(crate) parts_faked_count: IntCounter,
2892    pub(crate) parts_faked_bytes: IntCounter,
2893    pub(crate) parts_stats_trimmed_count: IntCounter,
2894    pub(crate) parts_stats_trimmed_bytes: IntCounter,
2895    pub(crate) parts_projection_trimmed_bytes: IntCounter,
2896    pub part_stats: PartStatsMetrics,
2897}
2898
2899impl PushdownMetrics {
2900    fn new(registry: &MetricsRegistry) -> Self {
2901        PushdownMetrics {
2902            parts_filtered_count: registry.register(metric!(
2903                name: "mz_persist_pushdown_parts_filtered_count",
2904                help: "count of parts filtered by pushdown",
2905            )),
2906            parts_filtered_bytes: registry.register(metric!(
2907                name: "mz_persist_pushdown_parts_filtered_bytes",
2908                help: "total size of parts filtered by pushdown in bytes",
2909            )),
2910            parts_fetched_count: registry.register(metric!(
2911                name: "mz_persist_pushdown_parts_fetched_count",
2912                help: "count of parts not filtered by pushdown",
2913            )),
2914            parts_fetched_bytes: registry.register(metric!(
2915                name: "mz_persist_pushdown_parts_fetched_bytes",
2916                help: "total size of parts not filtered by pushdown in bytes",
2917            )),
2918            parts_audited_count: registry.register(metric!(
2919                name: "mz_persist_pushdown_parts_audited_count",
2920                help: "count of parts fetched only for pushdown audit",
2921            )),
2922            parts_audited_bytes: registry.register(metric!(
2923                name: "mz_persist_pushdown_parts_audited_bytes",
2924                help: "total size of parts fetched only for pushdown audit",
2925            )),
2926            parts_inline_count: registry.register(metric!(
2927                name: "mz_persist_pushdown_parts_inline_count",
2928                help: "count of parts not fetched because they were inline",
2929            )),
2930            parts_inline_bytes: registry.register(metric!(
2931                name: "mz_persist_pushdown_parts_inline_bytes",
2932                help: "total size of parts not fetched because they were inline",
2933            )),
2934            parts_faked_count: registry.register(metric!(
2935                name: "mz_persist_pushdown_parts_faked_count",
2936                help: "count of parts faked because of aggressive projection pushdown",
2937            )),
2938            parts_faked_bytes: registry.register(metric!(
2939                name: "mz_persist_pushdown_parts_faked_bytes",
2940                help: "total size of parts replaced with fakes by aggressive projection pushdown",
2941            )),
2942            parts_stats_trimmed_count: registry.register(metric!(
2943                name: "mz_persist_pushdown_parts_stats_trimmed_count",
2944                help: "count of trimmed part stats",
2945            )),
2946            parts_stats_trimmed_bytes: registry.register(metric!(
2947                name: "mz_persist_pushdown_parts_stats_trimmed_bytes",
2948                help: "total bytes trimmed from part stats",
2949            )),
2950            parts_projection_trimmed_bytes: registry.register(metric!(
2951                name: "mz_persist_pushdown_parts_projection_trimmed_bytes",
2952                help: "total bytes trimmed from columnar data because of projection pushdown",
2953            )),
2954            part_stats: PartStatsMetrics::new(registry),
2955        }
2956    }
2957}
2958
2959#[derive(Debug)]
2960pub struct ConsolidationMetrics {
2961    pub(crate) parts_fetched: IntCounter,
2962    pub(crate) parts_skipped: IntCounter,
2963    pub(crate) parts_wasted: IntCounter,
2964    pub(crate) wrong_sort: IntCounter,
2965}
2966
2967impl ConsolidationMetrics {
2968    fn new(registry: &MetricsRegistry) -> Self {
2969        ConsolidationMetrics {
2970            parts_fetched: registry.register(metric!(
2971                name: "mz_persist_consolidation_parts_fetched_count",
2972                help: "count of parts that were fetched and used during consolidation",
2973            )),
2974            parts_skipped: registry.register(metric!(
2975                name: "mz_persist_consolidation_parts_skipped_count",
2976                help: "count of parts that were never needed during consolidation",
2977            )),
2978            parts_wasted: registry.register(metric!(
2979                name: "mz_persist_consolidation_parts_wasted_count",
2980                help: "count of parts that were fetched but not needed during consolidation",
2981            )),
2982            wrong_sort: registry.register(metric!(
2983                name: "mz_persist_consolidation_wrong_sort_count",
2984                help: "count of runs that were sorted using the wrong ordering for the current consolidation",
2985            )),
2986        }
2987    }
2988}
2989
2990#[derive(Debug)]
2991pub struct BlobMemCache {
2992    pub(crate) size_blobs: UIntGauge,
2993    pub(crate) size_bytes: UIntGauge,
2994    pub(crate) hits_blobs: IntCounter,
2995    pub(crate) hits_bytes: IntCounter,
2996    pub(crate) evictions: IntCounter,
2997}
2998
2999impl BlobMemCache {
3000    fn new(registry: &MetricsRegistry) -> Self {
3001        BlobMemCache {
3002            size_blobs: registry.register(metric!(
3003                name: "mz_persist_blob_cache_size_blobs",
3004                help: "count of blobs in the cache",
3005                const_labels: {"cache" => "mem"},
3006            )),
3007            size_bytes: registry.register(metric!(
3008                name: "mz_persist_blob_cache_size_bytes",
3009                help: "total size of blobs in the cache",
3010                const_labels: {"cache" => "mem"},
3011            )),
3012            hits_blobs: registry.register(metric!(
3013                name: "mz_persist_blob_cache_hits_blobs",
3014                help: "count of blobs served via cache instead of s3",
3015                const_labels: {"cache" => "mem"},
3016            )),
3017            hits_bytes: registry.register(metric!(
3018                name: "mz_persist_blob_cache_hits_bytes",
3019                help: "total size of blobs served via cache instead of s3",
3020                const_labels: {"cache" => "mem"},
3021            )),
3022            evictions: registry.register(metric!(
3023                name: "mz_persist_blob_cache_evictions",
3024                help: "count of capacity-based cache evictions",
3025                const_labels: {"cache" => "mem"},
3026            )),
3027        }
3028    }
3029}
3030
3031#[derive(Debug)]
3032pub struct SemaphoreMetrics {
3033    cfg: PersistConfig,
3034    registry: MetricsRegistry,
3035    fetch: OnceCell<MetricsSemaphore>,
3036}
3037
3038impl SemaphoreMetrics {
3039    fn new(cfg: PersistConfig, registry: MetricsRegistry) -> Self {
3040        SemaphoreMetrics {
3041            cfg,
3042            registry,
3043            fetch: OnceCell::new(),
3044        }
3045    }
3046
3047    /// We can't easily change the number of permits, and the dyncfgs are all
3048    /// set to defaults on process start, so make sure we only initialize the
3049    /// semaphore once we've synced dyncfgs at least once.
3050    async fn fetch(&self) -> &MetricsSemaphore {
3051        if let Some(x) = self.fetch.get() {
3052            // Common case of already initialized avoids the cloning below.
3053            return x;
3054        }
3055        let cfg = self.cfg.clone();
3056        let registry = self.registry.clone();
3057        let init = async move {
3058            let total_permits = match cfg.announce_memory_limit {
3059                // Non-cc replicas have the old physical flow control mechanism,
3060                // so only apply this one on cc replicas.
3061                Some(mem) if cfg.is_cc_active => {
3062                    // We can't easily adjust the number of permits later, so
3063                    // make sure we've synced dyncfg values at least once.
3064                    info!("fetch semaphore awaiting first dyncfg values");
3065                    let () = cfg.configs_synced_once().await;
3066                    let total_permits = usize::cast_lossy(
3067                        f64::cast_lossy(mem) * FETCH_SEMAPHORE_PERMIT_ADJUSTMENT.get(&cfg),
3068                    );
3069                    info!("fetch_semaphore got first dyncfg values");
3070                    total_permits
3071                }
3072                Some(_) | None => Semaphore::MAX_PERMITS,
3073            };
3074            MetricsSemaphore::new(&registry, "fetch", total_permits)
3075        };
3076        self.fetch.get_or_init(|| init).await
3077    }
3078
3079    pub(crate) async fn acquire_fetch_permits(&self, encoded_size_bytes: usize) -> MetricsPermits {
3080        // Adjust the requested permits to account for the difference between
3081        // encoded_size_bytes and the decoded size in lgalloc.
3082        let requested_permits = f64::cast_lossy(encoded_size_bytes);
3083        let requested_permits = requested_permits * FETCH_SEMAPHORE_COST_ADJUSTMENT.get(&self.cfg);
3084        let requested_permits = usize::cast_lossy(requested_permits);
3085        self.fetch().await.acquire_permits(requested_permits).await
3086    }
3087}
3088
3089#[derive(Debug)]
3090pub struct MetricsSemaphore {
3091    name: &'static str,
3092    semaphore: Arc<Semaphore>,
3093    total_permits: usize,
3094    acquire_count: IntCounter,
3095    blocking_count: IntCounter,
3096    blocking_seconds: Counter,
3097    acquired_permits: IntCounter,
3098    released_permits: IntCounter,
3099    _available_permits: ComputedUIntGauge,
3100}
3101
3102impl MetricsSemaphore {
3103    pub fn new(registry: &MetricsRegistry, name: &'static str, total_permits: usize) -> Self {
3104        let total_permits = std::cmp::min(total_permits, Semaphore::MAX_PERMITS);
3105        // TODO: Sadly, tokio::sync::Semaphore makes it difficult to have a
3106        // dynamic total_permits count.
3107        let semaphore = Arc::new(Semaphore::new(total_permits));
3108        MetricsSemaphore {
3109            name,
3110            total_permits,
3111            acquire_count: registry.register(metric!(
3112                name: "mz_persist_semaphore_acquire_count",
3113                help: "count of acquire calls (not acquired permits count)",
3114                const_labels: {"name" => name},
3115            )),
3116            blocking_count: registry.register(metric!(
3117                name: "mz_persist_semaphore_blocking_count",
3118                help: "count of acquire calls that had to block",
3119                const_labels: {"name" => name},
3120            )),
3121            blocking_seconds: registry.register(metric!(
3122                name: "mz_persist_semaphore_blocking_seconds",
3123                help: "total time spent blocking on permit acquisition",
3124                const_labels: {"name" => name},
3125            )),
3126            acquired_permits: registry.register(metric!(
3127                name: "mz_persist_semaphore_acquired_permits",
3128                help: "total sum of acquired permits",
3129                const_labels: {"name" => name},
3130            )),
3131            released_permits: registry.register(metric!(
3132                name: "mz_persist_semaphore_released_permits",
3133                help: "total sum of released permits",
3134                const_labels: {"name" => name},
3135            )),
3136            _available_permits: registry.register_computed_gauge(
3137                metric!(
3138                    name: "mz_persist_semaphore_available_permits",
3139                    help: "currently available permits according to the semaphore",
3140                ),
3141                {
3142                    let semaphore = Arc::clone(&semaphore);
3143                    move || u64::cast_from(semaphore.available_permits())
3144                },
3145            ),
3146            semaphore,
3147        }
3148    }
3149
3150    pub async fn acquire_permits(&self, requested_permits: usize) -> MetricsPermits {
3151        // HACK: Cap the request at the total permit count. This prevents
3152        // deadlock, even if the cfg gets set to some small value.
3153        let total_permits = u32::try_from(self.total_permits).unwrap_or(u32::MAX);
3154        let requested_permits = u32::try_from(requested_permits).unwrap_or(u32::MAX);
3155        let requested_permits = std::cmp::min(requested_permits, total_permits);
3156        let wrap = |_permit| {
3157            self.acquired_permits.inc_by(u64::from(requested_permits));
3158            MetricsPermits {
3159                _permit,
3160                released_metric: self.released_permits.clone(),
3161                count: requested_permits,
3162            }
3163        };
3164
3165        // Special-case non-blocking happy path.
3166        self.acquire_count.inc();
3167        match Arc::clone(&self.semaphore).try_acquire_many_owned(requested_permits) {
3168            Ok(x) => return wrap(x),
3169            Err(_) => {}
3170        };
3171
3172        // Sad path, gotta block.
3173        self.blocking_count.inc();
3174        let start = Instant::now();
3175        let ret = Arc::clone(&self.semaphore)
3176            .acquire_many_owned(requested_permits)
3177            .instrument(info_span!("acquire_permits"))
3178            .await;
3179        let elapsed = start.elapsed();
3180        self.blocking_seconds.inc_by(elapsed.as_secs_f64());
3181        debug!(
3182            "acquisition of {} {} permits blocked for {:?}",
3183            self.name, requested_permits, elapsed
3184        );
3185        wrap(ret.expect("semaphore is never closed"))
3186    }
3187}
3188
3189#[derive(Debug)]
3190pub struct MetricsPermits {
3191    _permit: OwnedSemaphorePermit,
3192    released_metric: IntCounter,
3193    count: u32,
3194}
3195
3196impl Drop for MetricsPermits {
3197    fn drop(&mut self) {
3198        self.released_metric.inc_by(u64::from(self.count))
3199    }
3200}
3201
3202#[derive(Debug)]
3203pub struct ExternalOpMetrics {
3204    started: IntCounter,
3205    succeeded: IntCounter,
3206    failed: IntCounter,
3207    bytes: IntCounter,
3208    seconds: Counter,
3209    seconds_histogram: Option<Histogram>,
3210    alerts_metrics: Arc<AlertsMetrics>,
3211}
3212
3213impl ExternalOpMetrics {
3214    async fn run_op<R, F, OpFn, ErrFn>(
3215        &self,
3216        op_fn: OpFn,
3217        on_err_fn: ErrFn,
3218    ) -> Result<R, ExternalError>
3219    where
3220        F: std::future::Future<Output = Result<R, ExternalError>>,
3221        OpFn: FnOnce() -> F,
3222        ErrFn: FnOnce(&AlertsMetrics, &ExternalError),
3223    {
3224        self.started.inc();
3225        let start = Instant::now();
3226        let res = op_fn().await;
3227        let elapsed_seconds = start.elapsed().as_secs_f64();
3228        self.seconds.inc_by(elapsed_seconds);
3229        if let Some(h) = &self.seconds_histogram {
3230            h.observe(elapsed_seconds);
3231        }
3232        match res.as_ref() {
3233            Ok(_) => self.succeeded.inc(),
3234            Err(err) => {
3235                self.failed.inc();
3236                on_err_fn(&self.alerts_metrics, err);
3237            }
3238        };
3239        res
3240    }
3241
3242    fn run_stream<'a, R: 'a, S, OpFn, ErrFn>(
3243        &'a self,
3244        op_fn: OpFn,
3245        mut on_err_fn: ErrFn,
3246    ) -> impl futures::Stream<Item = Result<R, ExternalError>> + 'a
3247    where
3248        S: futures::Stream<Item = Result<R, ExternalError>> + Unpin + 'a,
3249        OpFn: FnOnce() -> S,
3250        ErrFn: FnMut(&AlertsMetrics, &ExternalError) + 'a,
3251    {
3252        self.started.inc();
3253        let start = Instant::now();
3254        let mut stream = op_fn();
3255        stream! {
3256            let mut succeeded = true;
3257            while let Some(res) = stream.next().await {
3258                if let Err(err) = res.as_ref() {
3259                    on_err_fn(&self.alerts_metrics, err);
3260                    succeeded = false;
3261                }
3262                yield res;
3263            }
3264            if succeeded {
3265                self.succeeded.inc()
3266            } else {
3267                self.failed.inc()
3268            }
3269            let elapsed_seconds = start.elapsed().as_secs_f64();
3270            self.seconds.inc_by(elapsed_seconds);
3271            if let Some(h) = &self.seconds_histogram {
3272                h.observe(elapsed_seconds);
3273            }
3274        }
3275    }
3276}
3277
3278#[derive(Debug)]
3279pub struct BlobMetrics {
3280    set: ExternalOpMetrics,
3281    get: ExternalOpMetrics,
3282    list_keys: ExternalOpMetrics,
3283    delete: ExternalOpMetrics,
3284    restore: ExternalOpMetrics,
3285    delete_noop: IntCounter,
3286    blob_sizes: Histogram,
3287    pub rtt_latency: Gauge,
3288}
3289
3290#[derive(Debug)]
3291pub struct MetricsBlob {
3292    blob: Arc<dyn Blob>,
3293    metrics: Arc<Metrics>,
3294}
3295
3296impl MetricsBlob {
3297    pub fn new(blob: Arc<dyn Blob>, metrics: Arc<Metrics>) -> Self {
3298        MetricsBlob { blob, metrics }
3299    }
3300
3301    fn on_err(alerts_metrics: &AlertsMetrics, _err: &ExternalError) {
3302        alerts_metrics.blob_failures.inc()
3303    }
3304}
3305
3306#[async_trait]
3307impl Blob for MetricsBlob {
3308    #[instrument(name = "blob::get", fields(shard=blob_key_shard_id(key)))]
3309    async fn get(&self, key: &str) -> Result<Option<SegmentedBytes>, ExternalError> {
3310        let res = self
3311            .metrics
3312            .blob
3313            .get
3314            .run_op(|| self.blob.get(key), Self::on_err)
3315            .await;
3316        if let Ok(Some(value)) = res.as_ref() {
3317            self.metrics
3318                .blob
3319                .get
3320                .bytes
3321                .inc_by(u64::cast_from(value.len()));
3322        }
3323        res
3324    }
3325
3326    #[instrument(name = "blob::list_keys_and_metadata", fields(shard=blob_key_shard_id(key_prefix)))]
3327    async fn list_keys_and_metadata(
3328        &self,
3329        key_prefix: &str,
3330        f: &mut (dyn FnMut(BlobMetadata) + Send + Sync),
3331    ) -> Result<(), ExternalError> {
3332        let mut byte_total = 0;
3333        let mut instrumented = |blob_metadata: BlobMetadata| {
3334            // Track the size of the _keys_, not the blobs, so that we get a
3335            // sense for how much network bandwidth these calls are using.
3336            byte_total += blob_metadata.key.len();
3337            f(blob_metadata)
3338        };
3339
3340        let res = self
3341            .metrics
3342            .blob
3343            .list_keys
3344            .run_op(
3345                || {
3346                    self.blob
3347                        .list_keys_and_metadata(key_prefix, &mut instrumented)
3348                },
3349                Self::on_err,
3350            )
3351            .await;
3352
3353        self.metrics
3354            .blob
3355            .list_keys
3356            .bytes
3357            .inc_by(u64::cast_from(byte_total));
3358
3359        res
3360    }
3361
3362    #[instrument(name = "blob::set", fields(shard=blob_key_shard_id(key),size_bytes=value.len()))]
3363    async fn set(&self, key: &str, value: Bytes) -> Result<(), ExternalError> {
3364        let bytes = value.len();
3365        let res = self
3366            .metrics
3367            .blob
3368            .set
3369            .run_op(|| self.blob.set(key, value), Self::on_err)
3370            .await;
3371        if res.is_ok() {
3372            self.metrics.blob.set.bytes.inc_by(u64::cast_from(bytes));
3373            self.metrics.blob.blob_sizes.observe(f64::cast_lossy(bytes));
3374        }
3375        res
3376    }
3377
3378    #[instrument(name = "blob::delete", fields(shard=blob_key_shard_id(key)))]
3379    async fn delete(&self, key: &str) -> Result<Option<usize>, ExternalError> {
3380        let bytes = self
3381            .metrics
3382            .blob
3383            .delete
3384            .run_op(|| self.blob.delete(key), Self::on_err)
3385            .await?;
3386        if let Some(bytes) = bytes {
3387            self.metrics.blob.delete.bytes.inc_by(u64::cast_from(bytes));
3388        } else {
3389            self.metrics.blob.delete_noop.inc();
3390        }
3391        Ok(bytes)
3392    }
3393
3394    async fn restore(&self, key: &str) -> Result<(), ExternalError> {
3395        self.metrics
3396            .blob
3397            .restore
3398            .run_op(|| self.blob.restore(key), Self::on_err)
3399            .await
3400    }
3401}
3402
3403#[derive(Debug)]
3404pub struct ConsensusMetrics {
3405    list_keys: ExternalOpMetrics,
3406    head: ExternalOpMetrics,
3407    compare_and_set: ExternalOpMetrics,
3408    scan: ExternalOpMetrics,
3409    truncate: ExternalOpMetrics,
3410    truncated_count: IntCounter,
3411    pub rtt_latency: Gauge,
3412}
3413
3414#[derive(Debug)]
3415pub struct MetricsConsensus {
3416    consensus: Arc<dyn Consensus>,
3417    metrics: Arc<Metrics>,
3418}
3419
3420impl MetricsConsensus {
3421    pub fn new(consensus: Arc<dyn Consensus>, metrics: Arc<Metrics>) -> Self {
3422        MetricsConsensus { consensus, metrics }
3423    }
3424
3425    fn on_err(alerts_metrics: &AlertsMetrics, err: &ExternalError) {
3426        // As of 2022-09-06, regular determinate errors are expected in
3427        // Consensus (i.e. "txn conflict, please retry"), so only count the
3428        // indeterminate ones.
3429        if let ExternalError::Indeterminate(_) = err {
3430            alerts_metrics.consensus_failures.inc()
3431        }
3432    }
3433}
3434
3435#[async_trait]
3436impl Consensus for MetricsConsensus {
3437    fn list_keys(&self) -> ResultStream<'_, String> {
3438        Box::pin(
3439            self.metrics
3440                .consensus
3441                .list_keys
3442                .run_stream(|| self.consensus.list_keys(), Self::on_err),
3443        )
3444    }
3445
3446    #[instrument(name = "consensus::head", fields(shard=key))]
3447    async fn head(&self, key: &str) -> Result<Option<VersionedData>, ExternalError> {
3448        let res = self
3449            .metrics
3450            .consensus
3451            .head
3452            .run_op(|| self.consensus.head(key), Self::on_err)
3453            .await;
3454        if let Ok(Some(data)) = res.as_ref() {
3455            self.metrics
3456                .consensus
3457                .head
3458                .bytes
3459                .inc_by(u64::cast_from(data.data.len()));
3460        }
3461        res
3462    }
3463
3464    #[instrument(name = "consensus::compare_and_set", fields(shard=key,size_bytes=new.data.len()))]
3465    async fn compare_and_set(
3466        &self,
3467        key: &str,
3468        new: VersionedData,
3469    ) -> Result<CaSResult, ExternalError> {
3470        let bytes = new.data.len();
3471        let res = self
3472            .metrics
3473            .consensus
3474            .compare_and_set
3475            .run_op(|| self.consensus.compare_and_set(key, new), Self::on_err)
3476            .await;
3477        match res.as_ref() {
3478            Ok(CaSResult::Committed) => self
3479                .metrics
3480                .consensus
3481                .compare_and_set
3482                .bytes
3483                .inc_by(u64::cast_from(bytes)),
3484            Ok(CaSResult::ExpectationMismatch) | Err(_) => {}
3485        }
3486        res
3487    }
3488
3489    #[instrument(name = "consensus::scan", fields(shard=key))]
3490    async fn scan(
3491        &self,
3492        key: &str,
3493        from: SeqNo,
3494        limit: usize,
3495    ) -> Result<Vec<VersionedData>, ExternalError> {
3496        let res = self
3497            .metrics
3498            .consensus
3499            .scan
3500            .run_op(|| self.consensus.scan(key, from, limit), Self::on_err)
3501            .await;
3502        if let Ok(dataz) = res.as_ref() {
3503            let bytes: usize = dataz.iter().map(|x| x.data.len()).sum();
3504            self.metrics
3505                .consensus
3506                .scan
3507                .bytes
3508                .inc_by(u64::cast_from(bytes));
3509        }
3510        res
3511    }
3512
3513    #[instrument(name = "consensus::truncate", fields(shard=key))]
3514    async fn truncate(&self, key: &str, seqno: SeqNo) -> Result<Option<usize>, ExternalError> {
3515        let metrics = &self.metrics.consensus;
3516        let deleted = metrics
3517            .truncate
3518            .run_op(|| self.consensus.truncate(key, seqno), Self::on_err)
3519            .await?;
3520        if let Some(deleted) = deleted {
3521            metrics.truncated_count.inc_by(u64::cast_from(deleted));
3522        }
3523        Ok(deleted)
3524    }
3525}
3526
3527/// A standard set of metrics for an async task. Call [TaskMetrics::instrument_task] to instrument
3528/// a future and report its metrics for this task type.
3529#[derive(Debug, Clone)]
3530pub struct TaskMetrics {
3531    f64_gauges: Vec<(Gauge, fn(&tokio_metrics::TaskMetrics) -> f64)>,
3532    u64_gauges: Vec<(
3533        GenericGauge<AtomicU64>,
3534        fn(&tokio_metrics::TaskMetrics) -> u64,
3535    )>,
3536    monitor: TaskMonitor,
3537}
3538
3539impl TaskMetrics {
3540    pub fn new(name: &str) -> Self {
3541        let monitor = TaskMonitor::new();
3542        Self {
3543            f64_gauges: vec![
3544                (
3545                    Gauge::make_collector(metric!(
3546                        name: "mz_persist_task_total_idle_duration",
3547                        help: "Seconds of time spent idling, ie. waiting for a task to be woken up.",
3548                        const_labels: {"name" => name}
3549                    )),
3550                    |m| m.total_idle_duration.as_secs_f64(),
3551                ),
3552                (
3553                    Gauge::make_collector(metric!(
3554                        name: "mz_persist_task_total_scheduled_duration",
3555                        help: "Seconds of time spent scheduled, ie. ready to poll but not yet polled.",
3556                        const_labels: {"name" => name}
3557                    )),
3558                    |m| m.total_scheduled_duration.as_secs_f64(),
3559                ),
3560            ],
3561            u64_gauges: vec![
3562                (
3563                    MakeCollector::make_collector(metric!(
3564                        name: "mz_persist_task_total_scheduled_count",
3565                        help: "The total number of task schedules. Useful for computing the average scheduled time.",
3566                        const_labels: {"name" => name}
3567                    )),
3568                    |m| m.total_scheduled_count,
3569                ),
3570                (
3571                    MakeCollector::make_collector(metric!(
3572                        name: "mz_persist_task_total_idled_count",
3573                        help: "The total number of task idles. Useful for computing the average idle time.",
3574                        const_labels: {"name" => name}
3575                    ,
3576                    )),
3577                    |m| m.total_idled_count,
3578                ),
3579            ],
3580            monitor,
3581        }
3582    }
3583
3584    /// Instrument the provided future. The expectation is that the result will be executed
3585    /// as a task. (See [TaskMonitor::instrument] for more context.)
3586    pub fn instrument_task<F>(&self, task: F) -> tokio_metrics::Instrumented<F> {
3587        TaskMonitor::instrument(&self.monitor, task)
3588    }
3589}
3590
3591impl Collector for TaskMetrics {
3592    fn desc(&self) -> Vec<&Desc> {
3593        let mut descs = Vec::with_capacity(self.f64_gauges.len() + self.u64_gauges.len());
3594        for (g, _) in &self.f64_gauges {
3595            descs.extend(g.desc());
3596        }
3597        for (g, _) in &self.u64_gauges {
3598            descs.extend(g.desc());
3599        }
3600        descs
3601    }
3602
3603    fn collect(&self) -> Vec<MetricFamily> {
3604        let mut families = Vec::with_capacity(self.f64_gauges.len() + self.u64_gauges.len());
3605        let metrics = self.monitor.cumulative();
3606        for (g, metrics_fn) in &self.f64_gauges {
3607            g.set(metrics_fn(&metrics));
3608            families.extend(g.collect());
3609        }
3610        for (g, metrics_fn) in &self.u64_gauges {
3611            g.set(metrics_fn(&metrics));
3612            families.extend(g.collect());
3613        }
3614        families
3615    }
3616}
3617
3618#[derive(Debug)]
3619pub struct TasksMetrics {
3620    pub heartbeat_read: TaskMetrics,
3621}
3622
3623impl TasksMetrics {
3624    fn new(registry: &MetricsRegistry) -> Self {
3625        let heartbeat_read = TaskMetrics::new("heartbeat_read");
3626        registry.register_collector(heartbeat_read.clone());
3627        TasksMetrics { heartbeat_read }
3628    }
3629}
3630
3631#[derive(Debug)]
3632pub struct SchemaMetrics {
3633    pub(crate) cache_fetch_state_count: IntCounter,
3634    pub(crate) cache_schema: SchemaCacheMetrics,
3635    pub(crate) cache_migration: SchemaCacheMetrics,
3636    pub(crate) migration_count_same: IntCounter,
3637    pub(crate) migration_count_codec: IntCounter,
3638    pub(crate) migration_count_either: IntCounter,
3639    pub(crate) migration_len_legacy_codec: IntCounter,
3640    pub(crate) migration_len_either_codec: IntCounter,
3641    pub(crate) migration_len_either_arrow: IntCounter,
3642    pub(crate) migration_new_count: IntCounter,
3643    pub(crate) migration_new_seconds: Counter,
3644    pub(crate) migration_migrate_seconds: Counter,
3645}
3646
3647impl SchemaMetrics {
3648    fn new(registry: &MetricsRegistry) -> Self {
3649        let cached: IntCounterVec = registry.register(metric!(
3650            name: "mz_persist_schema_cache_cached_count",
3651            help: "count of schema cache entries served from cache",
3652            var_labels: ["op"],
3653        ));
3654        let computed: IntCounterVec = registry.register(metric!(
3655            name: "mz_persist_schema_cache_computed_count",
3656            help: "count of schema cache entries computed",
3657            var_labels: ["op"],
3658        ));
3659        let unavailable: IntCounterVec = registry.register(metric!(
3660            name: "mz_persist_schema_cache_unavailable_count",
3661            help: "count of schema cache entries unavailable at current state",
3662            var_labels: ["op"],
3663        ));
3664        let added: IntCounterVec = registry.register(metric!(
3665            name: "mz_persist_schema_cache_added_count",
3666            help: "count of schema cache entries added",
3667            var_labels: ["op"],
3668        ));
3669        let dropped: IntCounterVec = registry.register(metric!(
3670            name: "mz_persist_schema_cache_dropped_count",
3671            help: "count of schema cache entries dropped",
3672            var_labels: ["op"],
3673        ));
3674        let cache = |name| SchemaCacheMetrics {
3675            cached_count: cached.with_label_values(&[name]),
3676            computed_count: computed.with_label_values(&[name]),
3677            unavailable_count: unavailable.with_label_values(&[name]),
3678            added_count: added.with_label_values(&[name]),
3679            dropped_count: dropped.with_label_values(&[name]),
3680        };
3681        let migration_count: IntCounterVec = registry.register(metric!(
3682            name: "mz_persist_schema_migration_count",
3683            help: "count of fetch part migrations",
3684            var_labels: ["op"],
3685        ));
3686        let migration_len: IntCounterVec = registry.register(metric!(
3687            name: "mz_persist_schema_migration_len",
3688            help: "count of migrated update records",
3689            var_labels: ["op"],
3690        ));
3691        SchemaMetrics {
3692            cache_fetch_state_count: registry.register(metric!(
3693                name: "mz_persist_schema_cache_fetch_state_count",
3694                help: "count of state fetches by the schema cache",
3695            )),
3696            cache_schema: cache("schema"),
3697            cache_migration: cache("migration"),
3698            migration_count_same: migration_count.with_label_values(&["same"]),
3699            migration_count_codec: migration_count.with_label_values(&["codec"]),
3700            migration_count_either: migration_count.with_label_values(&["either"]),
3701            migration_len_legacy_codec: migration_len.with_label_values(&["legacy_codec"]),
3702            migration_len_either_codec: migration_len.with_label_values(&["either_codec"]),
3703            migration_len_either_arrow: migration_len.with_label_values(&["either_arrow"]),
3704            migration_new_count: registry.register(metric!(
3705                name: "mz_persist_schema_migration_new_count",
3706                help: "count of migrations constructed",
3707            )),
3708            migration_new_seconds: registry.register(metric!(
3709                name: "mz_persist_schema_migration_new_seconds",
3710                help: "seconds spent constructing migration logic",
3711            )),
3712            migration_migrate_seconds: registry.register(metric!(
3713                name: "mz_persist_schema_migration_migrate_seconds",
3714                help: "seconds spent applying migration logic",
3715            )),
3716        }
3717    }
3718}
3719
3720#[derive(Debug, Clone)]
3721pub struct SchemaCacheMetrics {
3722    pub(crate) cached_count: IntCounter,
3723    pub(crate) computed_count: IntCounter,
3724    pub(crate) unavailable_count: IntCounter,
3725    pub(crate) added_count: IntCounter,
3726    pub(crate) dropped_count: IntCounter,
3727}
3728
3729#[derive(Debug)]
3730pub struct InlineMetrics {
3731    pub(crate) part_commit_count: IntCounter,
3732    pub(crate) part_commit_bytes: IntCounter,
3733    pub(crate) backpressure: BatchWriteMetrics,
3734}
3735
3736impl InlineMetrics {
3737    fn new(registry: &MetricsRegistry) -> Self {
3738        InlineMetrics {
3739            part_commit_count: registry.register(metric!(
3740                name: "mz_persist_inline_part_commit_count",
3741                help: "count of inline parts committed to state",
3742            )),
3743            part_commit_bytes: registry.register(metric!(
3744                name: "mz_persist_inline_part_commit_bytes",
3745                help: "total size of of inline parts committed to state",
3746            )),
3747            backpressure: BatchWriteMetrics::new(registry, "inline_backpressure"),
3748        }
3749    }
3750}
3751
3752fn blob_key_shard_id(key: &str) -> Option<String> {
3753    let (shard_id, _) = BlobKey::parse_ids(key).ok()?;
3754    Some(shard_id.to_string())
3755}
3756
3757/// Encode a frontier into an i64 acceptable for use in metrics.
3758pub fn encode_ts_metric<T: Codec64>(ts: &Antichain<T>) -> i64 {
3759    // We have two problems in mapping a persist frontier into a metric.
3760    // First is that we only have a `T: Timestamp+Codec64`. Second, is
3761    // mapping an antichain to a single counter value. We solve both by
3762    // taking advantage of the fact that in practice, timestamps in mz are
3763    // currently always a u64 (and if we switch them, it will be to an i64).
3764    // This means that for all values that mz would actually produce,
3765    // interpreting the encoded bytes as a little-endian i64 will work.
3766    // Both of them impl PartialOrder, so in practice, there will always be
3767    // zero or one elements in the antichain.
3768    match ts.elements().first() {
3769        Some(ts) => i64::from_le_bytes(Codec64::encode(ts)),
3770        None => i64::MAX,
3771    }
3772}
3773
3774#[cfg(test)]
3775mod tests {
3776    use std::collections::BTreeSet;
3777
3778    use mz_dyncfg::ConfigUpdates;
3779    use mz_ore::metrics::aggregation::PERCENTILES;
3780
3781    use crate::cfg::SHARD_METRICS;
3782
3783    use super::*;
3784
3785    #[mz_ore::test]
3786    fn shards_aggregate_metrics_one_pass() {
3787        let registry = MetricsRegistry::new();
3788        let configs = Arc::clone(&PersistConfig::new_for_tests().configs);
3789        let shards = ShardsMetrics::new(&registry, Arc::clone(&configs));
3790        let agg = ShardsAggregateMetrics::new(Arc::clone(&shards.shards), configs);
3791        let a = shards.shard(&ShardId::new(), "a");
3792        let b = shards.shard(&ShardId::new(), "b");
3793
3794        // collect() recomputes both gauges from a single walk of the map.
3795        agg.collect();
3796        assert_eq!(agg.count.get(), 2);
3797        assert_eq!(agg.stale_count.get(), 0);
3798
3799        a.stale.store(true, Ordering::Relaxed);
3800        agg.collect();
3801        assert_eq!(agg.count.get(), 2);
3802        assert_eq!(agg.stale_count.get(), 1);
3803
3804        b.stale.store(true, Ordering::Relaxed);
3805        agg.collect();
3806        assert_eq!(agg.stale_count.get(), 2);
3807
3808        // Flipping back and dropping a handle both lower the counts: the gauge
3809        // is recomputed, not ticked.
3810        a.stale.store(false, Ordering::Relaxed);
3811        drop(b);
3812        agg.collect();
3813        assert_eq!(agg.count.get(), 1);
3814        assert_eq!(agg.stale_count.get(), 0);
3815    }
3816
3817    #[mz_ore::test]
3818    fn per_shard_families_match_registrations() {
3819        let cfg = PersistConfig::new_for_tests();
3820        set_shard_metrics(&cfg, ShardMetricsExport::Both);
3821        let registry = MetricsRegistry::new();
3822        let metrics = Metrics::new(&cfg, &registry);
3823        let shard = metrics.shards.shard(&ShardId::new(), "s");
3824        shard.set_batch_part_versions(["v1"].into_iter());
3825        let gathered = registry.gather();
3826
3827        let registered: BTreeSet<_> = gathered
3828            .iter()
3829            .filter(|f| !f.name().ends_with("_topk"))
3830            .filter(|f| {
3831                f.get_metric()
3832                    .iter()
3833                    .any(|m| m.get_label().iter().any(|l| l.name() == "shard"))
3834            })
3835            .map(|f| f.name())
3836            .collect();
3837        let listed: BTreeSet<_> = PER_SHARD_FAMILIES.iter().map(|f| f.name).collect();
3838        assert_eq!(registered, listed);
3839
3840        // `ShardGauge::help` is a second copy of the registered help, and
3841        // becomes the `_percentile` / `_topk` help. Keep the wording the same.
3842        for (name, gauge) in shard_gauges() {
3843            let registered = find(&gathered, name)
3844                .unwrap_or_else(|| panic!("{name} not gathered"))
3845                .help();
3846            assert_eq!(gauge.help, registered, "{name}");
3847        }
3848
3849        let gathered: BTreeSet<_> = gathered.iter().map(|f| f.name()).collect();
3850        for family in PER_SHARD_FAMILIES {
3851            if let ShardAggregate::Counter { total } = family.aggregate {
3852                assert!(gathered.contains(total), "{total} not gathered");
3853            }
3854        }
3855    }
3856
3857    #[mz_ore::test]
3858    fn every_shard_gauge_has_its_aggregates() {
3859        let registry = MetricsRegistry::new();
3860        let configs = Arc::clone(&PersistConfig::new_for_tests().configs);
3861        let shards = ShardsMetrics::new(&registry, Arc::clone(&configs));
3862        let agg = ShardsAggregateMetrics::new(Arc::clone(&shards.shards), configs);
3863        let declared: BTreeSet<_> = agg.desc().iter().map(|d| d.fq_name.clone()).collect();
3864        for (gauge, g) in shard_gauges() {
3865            for suffix in ["_percentile", "_topk"] {
3866                let name = format!("{gauge}{suffix}");
3867                assert!(declared.contains(&name), "{name} not declared");
3868            }
3869            let sum = format!("{gauge}_sum");
3870            assert_eq!(declared.contains(&sum), g.sum, "{sum}");
3871        }
3872    }
3873
3874    fn find<'a>(families: &'a [MetricFamily], name: &str) -> Option<&'a MetricFamily> {
3875        families.iter().find(|f| f.name() == name)
3876    }
3877
3878    fn label<'a>(m: &'a prometheus::proto::Metric, name: &str) -> &'a str {
3879        m.get_label()
3880            .iter()
3881            .find(|l| l.name() == name)
3882            .map(|l| l.value())
3883            .unwrap_or_else(|| panic!("no label {name}"))
3884    }
3885
3886    /// The `(label, value)` pairs of a gauge family, as a set since gather
3887    /// order is not insertion order.
3888    fn gauge_set(
3889        families: &[MetricFamily],
3890        name: &str,
3891        label_name: &str,
3892    ) -> BTreeSet<(String, u64)> {
3893        find(families, name).map_or_else(BTreeSet::new, |f| {
3894            f.get_metric()
3895                .iter()
3896                .map(|m| {
3897                    (
3898                        label(m, label_name).to_string(),
3899                        u64::cast_lossy(m.get_gauge().value()),
3900                    )
3901                })
3902                .collect()
3903        })
3904    }
3905
3906    fn scalar(families: &[MetricFamily], name: &str) -> f64 {
3907        let f = find(families, name).unwrap_or_else(|| panic!("{name} not gathered"));
3908        let m = &f.get_metric()[0];
3909        if f.get_field_type() == prometheus::proto::MetricType::COUNTER {
3910            m.get_counter().value()
3911        } else {
3912            m.get_gauge().value()
3913        }
3914    }
3915
3916    /// Builds `Metrics` on a fresh registry with `n` shards that populate
3917    /// every per-shard family.
3918    fn metrics_with_shards(n: u64) -> (PersistConfig, MetricsRegistry, Vec<Arc<ShardMetrics>>) {
3919        let cfg = PersistConfig::new_for_tests();
3920        let registry = MetricsRegistry::new();
3921        let metrics = Metrics::new(&cfg, &registry);
3922        let shards = (1..=n)
3923            .map(|i| {
3924                let shard = metrics.shards.shard(&ShardId::new(), &format!("s{i}"));
3925                shard.usage_current_state_batches_bytes.set(i);
3926                shard.set_batch_part_versions(["v1"].into_iter());
3927                shard
3928            })
3929            .collect();
3930        (cfg, registry, shards)
3931    }
3932
3933    fn set_shard_metrics(cfg: &PersistConfig, mode: ShardMetricsExport) {
3934        let mut updates = ConfigUpdates::default();
3935        updates.add(&SHARD_METRICS, mode.as_str());
3936        cfg.apply_from(&updates);
3937    }
3938
3939    fn family_names(families: &[MetricFamily]) -> BTreeSet<String> {
3940        families.iter().map(|f| f.name().to_string()).collect()
3941    }
3942
3943    /// The families that stay in every mode: the shard counts and the
3944    /// process-level counters the per-shard counters roll up into.
3945    fn always_exported() -> BTreeSet<String> {
3946        let mut names: BTreeSet<_> = ["mz_persist_shard_count", "mz_persist_stale_shard_count"]
3947            .into_iter()
3948            .map(String::from)
3949            .collect();
3950        names.extend(PER_SHARD_FAMILIES.iter().filter_map(|f| match f.aggregate {
3951            ShardAggregate::Counter { total } => Some(total.to_string()),
3952            _ => None,
3953        }));
3954        names
3955    }
3956
3957    #[mz_ore::test]
3958    fn shard_metrics_modes_round_trip() {
3959        for mode in ShardMetricsExport::ALL {
3960            assert_eq!(ShardMetricsExport::parse(mode.as_str()), Some(mode));
3961        }
3962        assert_eq!(
3963            ShardMetricsExport::parse(SHARD_METRICS.default()),
3964            Some(ShardMetricsExport::default())
3965        );
3966        assert_eq!(ShardMetricsExport::parse("not_a_mode"), None);
3967    }
3968
3969    #[mz_ore::test]
3970    fn unknown_shard_metrics_mode_exports_the_default() {
3971        let (cfg, registry, _shards) = metrics_with_shards(3);
3972        let summary = family_names(&registry.gather());
3973        let invalid =
3974            |families: &[MetricFamily]| scalar(families, "mz_persist_shard_metrics_mode_invalid");
3975
3976        let mut updates = ConfigUpdates::default();
3977        updates.add(&SHARD_METRICS, "summry");
3978        cfg.apply_from(&updates);
3979
3980        let gathered = registry.gather();
3981        assert_eq!(family_names(&gathered), summary);
3982        // Counted on every scrape while it stays unrecognized, one scrape late.
3983        assert_eq!(invalid(&gathered), 0.0);
3984        assert_eq!(invalid(&registry.gather()), 1.0);
3985
3986        set_shard_metrics(&cfg, ShardMetricsExport::Both);
3987        let gathered = registry.gather();
3988        assert!(summary.is_subset(&family_names(&gathered)));
3989        assert_eq!(invalid(&gathered), 2.0);
3990        assert_eq!(invalid(&registry.gather()), 2.0);
3991    }
3992
3993    #[mz_ore::test]
3994    fn counter_totals_are_unchanged_by_the_mode() {
3995        let (cfg, registry, shards) = metrics_with_shards(3);
3996        for (i, shard) in shards.iter().enumerate() {
3997            shard.blob_gets.inc_by(u64::cast_from(i) + 1);
3998            shard.blob_sets.inc();
3999        }
4000        let totals = |families: &[MetricFamily]| -> BTreeMap<&'static str, f64> {
4001            PER_SHARD_FAMILIES
4002                .iter()
4003                .filter_map(|f| match f.aggregate {
4004                    ShardAggregate::Counter { total } => Some((total, scalar(families, total))),
4005                    _ => None,
4006                })
4007                .collect()
4008        };
4009        let both = registry.gather();
4010        assert_eq!(totals(&both)["mz_persist_blob_gets"], 6.0);
4011
4012        // The modes drop the per-shard families. The totals that replace
4013        // them don't move.
4014        for mode in ShardMetricsExport::ALL {
4015            set_shard_metrics(&cfg, mode);
4016            assert_eq!(totals(&registry.gather()), totals(&both), "{mode:?}");
4017        }
4018    }
4019
4020    #[mz_ore::test]
4021    fn each_shard_metrics_mode_exports_its_families() {
4022        let (cfg, registry, _shards) = metrics_with_shards(3);
4023        set_shard_metrics(&cfg, ShardMetricsExport::Both);
4024        let both = family_names(&registry.gather());
4025        let per_shard: BTreeSet<_> = PER_SHARD_FAMILIES
4026            .iter()
4027            .map(|f| f.name.to_string())
4028            .collect();
4029        let summary: BTreeSet<_> = both
4030            .iter()
4031            .filter(|n| {
4032                n.ends_with("_percentile")
4033                    || n.ends_with("_sum")
4034                    || n.ends_with("_topk")
4035                    || *n == "mz_persist_batch_part_version_count"
4036            })
4037            .cloned()
4038            .collect();
4039        let always = always_exported();
4040        assert!(both.is_superset(&per_shard));
4041        assert!(both.is_superset(&always));
4042        assert!(!summary.is_empty());
4043        assert!(always.is_disjoint(&per_shard) && always.is_disjoint(&summary));
4044
4045        // The mode is read on every gather, so switching it needs no new
4046        // `Metrics`. `ALL` ends with `Both`, which restores everything.
4047        for mode in ShardMetricsExport::ALL {
4048            set_shard_metrics(&cfg, mode);
4049            let mut expected = both.clone();
4050            if !mode.per_shard() {
4051                expected.retain(|n| !per_shard.contains(n));
4052            }
4053            if !mode.summary() {
4054                expected.retain(|n| !summary.contains(n));
4055            }
4056            assert!(expected.is_superset(&always), "{mode:?}");
4057            assert_eq!(family_names(&registry.gather()), expected, "{mode:?}");
4058        }
4059    }
4060
4061    #[mz_ore::test]
4062    #[cfg_attr(miri, ignore)] // too slow
4063    fn summary_scrape_is_flat_in_shard_count() {
4064        let scrape = |n: u64, mode: ShardMetricsExport| {
4065            let (cfg, registry, _shards) = metrics_with_shards(n);
4066            set_shard_metrics(&cfg, mode);
4067            let families = registry.gather();
4068            let series: usize = families.iter().map(|f| f.get_metric().len()).sum();
4069            let body = prometheus::TextEncoder::new()
4070                .encode_to_string(&families)
4071                .expect("encodable");
4072            (series, body.len())
4073        };
4074        let (few_series, few_bytes) = scrape(10, ShardMetricsExport::Summary);
4075        let (many_series, many_bytes) = scrape(10_000, ShardMetricsExport::Summary);
4076        let (both_series, both_bytes) = scrape(10_000, ShardMetricsExport::Both);
4077        assert_eq!(few_series, many_series);
4078        assert!(
4079            both_series >= many_series + PER_SHARD_FAMILIES.len() * 10_000,
4080            "{both_series} vs {many_series}"
4081        );
4082        // Only the digits of the aggregated values grow with the shard count.
4083        assert!(many_bytes < few_bytes + 4096, "{many_bytes} vs {few_bytes}");
4084        assert!(
4085            both_bytes > 100 * many_bytes,
4086            "{both_bytes} vs {many_bytes}"
4087        );
4088    }
4089
4090    #[mz_ore::test]
4091    fn shard_info_lists_live_shards_when_enabled() {
4092        let (cfg, registry, mut shards) = metrics_with_shards(3);
4093        let info =
4094            |registry: &MetricsRegistry| gauge_set(&registry.gather(), SHARD_INFO_NAME, "name");
4095        let mut updates = ConfigUpdates::default();
4096        updates.add(&SHARD_INFO_METRIC, true);
4097        cfg.apply_from(&updates);
4098        // Independent of the mode, including the one that drops everything else.
4099        for mode in ShardMetricsExport::ALL {
4100            set_shard_metrics(&cfg, mode);
4101            let expected = ["s1", "s2", "s3"].map(|n| (n.to_string(), 1)).into();
4102            assert_eq!(info(&registry), expected, "{mode:?}");
4103        }
4104
4105        shards.pop();
4106        assert_eq!(info(&registry).len(), 2);
4107    }
4108
4109    fn set_enable_regex(cfg: &PersistConfig, regex: &str) {
4110        let mut updates = ConfigUpdates::default();
4111        updates.add(&PER_SHARD_METRICS_ENABLE_REGEX, regex);
4112        cfg.apply_from(&updates);
4113    }
4114
4115    /// The per-shard series in `families`, keyed by family name and labels.
4116    fn per_shard_series(
4117        families: &[MetricFamily],
4118    ) -> BTreeMap<(String, Vec<(String, String)>), prometheus::proto::Metric> {
4119        families
4120            .iter()
4121            .filter(|f| PER_SHARD_FAMILIES.iter().any(|p| p.name == f.name()))
4122            .flat_map(|f| {
4123                f.get_metric().iter().map(|m| {
4124                    let labels = m
4125                        .get_label()
4126                        .iter()
4127                        .map(|l| (l.name().to_string(), l.value().to_string()))
4128                        .collect();
4129                    ((f.name().to_string(), labels), m.clone())
4130                })
4131            })
4132            .collect()
4133    }
4134
4135    /// The `name` labels of the shards with per-shard series in `families`.
4136    fn exported_shard_names(families: &[MetricFamily]) -> BTreeSet<String> {
4137        per_shard_series(families)
4138            .into_keys()
4139            .map(|(_, labels)| {
4140                labels
4141                    .into_iter()
4142                    .find(|(k, _)| k == "name")
4143                    .expect("per-shard series have a name label")
4144                    .1
4145            })
4146            .collect()
4147    }
4148
4149    #[mz_ore::test]
4150    fn enable_regex_keeps_matching_shards_unchanged() {
4151        let (cfg, registry, shards) = metrics_with_shards(5);
4152        set_shard_metrics(&cfg, ShardMetricsExport::Both);
4153        let both = registry.gather();
4154        set_shard_metrics(&cfg, ShardMetricsExport::Summary);
4155        let summary = registry.gather();
4156        assert!(exported_shard_names(&summary).is_empty());
4157
4158        // By shard id: exactly that shard's series come back, byte for byte
4159        // as they are in `both`, so dashboards need no change.
4160        let shard_id = shards[2].shard_id.to_string();
4161        set_enable_regex(&cfg, &shard_id);
4162        let enabled = registry.gather();
4163        let expected: BTreeMap<_, _> = per_shard_series(&both)
4164            .into_iter()
4165            .filter(|((_, labels), _)| labels.contains(&("shard".into(), shard_id.clone())))
4166            .collect();
4167        assert_eq!(expected.len(), PER_SHARD_FAMILIES.len());
4168        assert_eq!(per_shard_series(&enabled), expected);
4169        assert_eq!(family_names(&enabled), {
4170            let mut names = family_names(&summary);
4171            names.extend(PER_SHARD_FAMILIES.iter().map(|f| f.name.to_string()));
4172            names
4173        });
4174
4175        // By `name` label, and several at once.
4176        set_enable_regex(&cfg, "^s2$");
4177        assert_eq!(
4178            exported_shard_names(&registry.gather()),
4179            BTreeSet::from(["s2".to_string()])
4180        );
4181        set_enable_regex(&cfg, "^s(1|4)$");
4182        assert_eq!(
4183            exported_shard_names(&registry.gather()),
4184            BTreeSet::from(["s1".to_string(), "s4".to_string()])
4185        );
4186
4187        // Empty keeps none again.
4188        set_enable_regex(&cfg, "");
4189        assert_eq!(family_names(&registry.gather()), family_names(&summary));
4190
4191        // It applies in `none` too, and not while per-shard series are on.
4192        set_enable_regex(&cfg, "^s2$");
4193        set_shard_metrics(&cfg, ShardMetricsExport::None);
4194        let gathered = registry.gather();
4195        assert_eq!(
4196            exported_shard_names(&gathered),
4197            BTreeSet::from(["s2".to_string()])
4198        );
4199        assert!(gathered.iter().all(|f| !f.name().ends_with("_percentile")));
4200        set_shard_metrics(&cfg, ShardMetricsExport::Both);
4201        assert_eq!(exported_shard_names(&registry.gather()).len(), 5);
4202    }
4203
4204    #[mz_ore::test]
4205    fn unanchored_enable_regex_over_matches() {
4206        let cfg = PersistConfig::new_for_tests();
4207        let registry = MetricsRegistry::new();
4208        let metrics = Metrics::new(&cfg, &registry);
4209        let shard = |name: &str| metrics.shards.shard(&ShardId::new(), name);
4210        // `r` is not a hex digit, so these names can't collide with the
4211        // random shard ids the second half matches against.
4212        let _named: Vec<_> = (1..=10).map(|i| shard(&format!("src{i}"))).collect();
4213        set_shard_metrics(&cfg, ShardMetricsExport::Summary);
4214
4215        // Unanchored, so a pattern meant for one shard keeps every name it
4216        // is a substring of.
4217        set_enable_regex(&cfg, "src1");
4218        assert_eq!(
4219            exported_shard_names(&registry.gather()),
4220            BTreeSet::from(["src1".to_string(), "src10".to_string()])
4221        );
4222
4223        // It is tried against `shard` and `name`, so a pattern scoped to one
4224        // shard's id also keeps any shard whose name contains that fragment.
4225        // The naming here is contrived, but a pattern loose enough to hit
4226        // both labels is what brings the shed cardinality back.
4227        let target = shard("by-id");
4228        let fragment = target.shard_id.to_string()[3..9].to_string();
4229        let decoy = shard(&format!("t-{fragment}-t"));
4230        assert!(!decoy.shard_id.to_string().contains(&fragment));
4231        set_enable_regex(&cfg, &fragment);
4232        let kept = exported_shard_names(&registry.gather());
4233        assert!(kept.contains(&target.name), "{kept:?}");
4234        assert!(kept.contains(&decoy.name), "{kept:?}");
4235    }
4236
4237    #[mz_ore::test]
4238    fn invalid_enable_regex_fails_closed() {
4239        let (cfg, registry, _shards) = metrics_with_shards(3);
4240        set_shard_metrics(&cfg, ShardMetricsExport::Summary);
4241        let summary = family_names(&registry.gather());
4242        let invalid = |families: &[MetricFamily]| {
4243            scalar(families, "mz_persist_per_shard_metrics_regex_invalid")
4244        };
4245
4246        set_enable_regex(&cfg, "(");
4247        let gathered = registry.gather();
4248        assert_eq!(family_names(&gathered), summary);
4249        // Counted on every scrape while it stays invalid, one scrape late.
4250        assert_eq!(invalid(&gathered), 0.0);
4251        assert_eq!(invalid(&registry.gather()), 1.0);
4252
4253        set_enable_regex(&cfg, "^s1$");
4254        let gathered = registry.gather();
4255        assert_eq!(
4256            exported_shard_names(&gathered),
4257            BTreeSet::from(["s1".to_string()])
4258        );
4259        assert_eq!(invalid(&gathered), 2.0);
4260        assert_eq!(invalid(&registry.gather()), 2.0);
4261    }
4262
4263    fn gauge_count(families: &[MetricFamily], name: &str) -> usize {
4264        find(families, name).map_or(0, |f| f.get_metric().len())
4265    }
4266
4267    /// The value of one series of a `_percentile` family.
4268    fn percentile(families: &[MetricFamily], name: &str, percentile: &str) -> u64 {
4269        gauge_set(families, name, "percentile")
4270            .into_iter()
4271            .find_map(|(p, v)| (p == percentile).then_some(v))
4272            .unwrap_or_else(|| panic!("no {percentile} in {name}"))
4273    }
4274
4275    #[mz_ore::test]
4276    fn shard_aggregates_fold_live_shards() {
4277        let registry = MetricsRegistry::new();
4278        let metrics = Metrics::new(&PersistConfig::new_for_tests(), &registry);
4279        let mut shards: Vec<_> = (1..=12u64)
4280            .map(|i| {
4281                let shard = metrics.shards.shard(&ShardId::new(), &format!("s{i}"));
4282                shard.usage_current_state_batches_bytes.set(i * 100);
4283                shard
4284            })
4285            .collect();
4286        shards[0].set_batch_part_versions(["v1", "v1", "v2"].into_iter());
4287        shards[1].set_batch_part_versions(["v1"].into_iter());
4288
4289        let gathered = registry.gather();
4290        assert_eq!(scalar(&gathered, "mz_persist_shard_count"), 12.0);
4291        let percentile_families = gathered
4292            .iter()
4293            .filter(|f| {
4294                f.name().starts_with("mz_persist_shard_") && f.name().ends_with("_percentile")
4295            })
4296            .count();
4297        assert_eq!(percentile_families, shard_gauges().count());
4298        for (gauge, _) in shard_gauges() {
4299            let name = format!("{gauge}_percentile");
4300            assert_eq!(gauge_count(&gathered, &name), PERCENTILES.len(), "{name}");
4301        }
4302        let percentiles = "mz_persist_shard_usage_current_state_batches_bytes_percentile";
4303        assert_eq!(percentile(&gathered, percentiles, "min"), 100);
4304        assert_eq!(percentile(&gathered, percentiles, "p50"), 600);
4305        assert_eq!(percentile(&gathered, percentiles, "max"), 1200);
4306        let top_k = "mz_persist_shard_usage_current_state_batches_bytes_topk";
4307        let expected: BTreeSet<_> = shards[2..]
4308            .iter()
4309            .map(|s| {
4310                (
4311                    s.shard_id.to_string(),
4312                    s.usage_current_state_batches_bytes.get(),
4313                )
4314            })
4315            .collect();
4316        assert_eq!(gauge_set(&gathered, top_k, "shard"), expected);
4317        assert_eq!(
4318            gauge_set(&gathered, "mz_persist_batch_part_version_count", "version"),
4319            BTreeSet::from([("v1".to_string(), 3), ("v2".to_string(), 1)])
4320        );
4321
4322        // Dropping the largest shard removes it from the next gather, and a
4323        // version no shard has anymore is not exported.
4324        let largest = shards.pop().expect("non-empty");
4325        let largest_id = largest.shard_id.to_string();
4326        drop(largest);
4327        shards[0].set_batch_part_versions(["v1"].into_iter());
4328        let gathered = registry.gather();
4329        assert_eq!(scalar(&gathered, "mz_persist_shard_count"), 11.0);
4330        assert_eq!(percentile(&gathered, percentiles, "max"), 1100);
4331        let top_k_shards = gauge_set(&gathered, top_k, "shard");
4332        assert_eq!(top_k_shards.len(), SHARD_TOP_K);
4333        assert!(top_k_shards.iter().all(|(shard, _)| *shard != largest_id));
4334        assert_eq!(
4335            gauge_set(&gathered, "mz_persist_batch_part_version_count", "version"),
4336            BTreeSet::from([("v1".to_string(), 2)])
4337        );
4338    }
4339
4340    #[mz_ore::test]
4341    fn process_counters_survive_shard_drop() {
4342        let cfg = PersistConfig::new_for_tests();
4343        set_shard_metrics(&cfg, ShardMetricsExport::Both);
4344        let registry = MetricsRegistry::new();
4345        let metrics = Metrics::new(&cfg, &registry);
4346        let a = metrics.shards.shard(&ShardId::new(), "a");
4347        let b = metrics.shards.shard(&ShardId::new(), "b");
4348        a.blob_gets.inc_by(5);
4349        b.blob_gets.inc();
4350        a.encoded_diff_size.inc_by(100);
4351
4352        let gathered = registry.gather();
4353        assert_eq!(scalar(&gathered, "mz_persist_blob_gets"), 6.0);
4354        assert_eq!(gauge_count(&gathered, "mz_persist_shard_blob_gets"), 2);
4355
4356        drop(a);
4357        let gathered = registry.gather();
4358        assert_eq!(scalar(&gathered, "mz_persist_blob_gets"), 6.0);
4359        assert_eq!(scalar(&gathered, "mz_persist_diff_size_bytes"), 100.0);
4360        assert_eq!(gauge_count(&gathered, "mz_persist_shard_blob_gets"), 1);
4361    }
4362}