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    pub(crate) hollow_run_missing_on_delete: IntCounter,
1202    pub(crate) finalized_rollup_assigned: IntCounter,
1203}
1204
1205impl StateMetrics {
1206    pub(crate) fn new(registry: &MetricsRegistry) -> Self {
1207        let rollup_write_noop: IntCounterVec = registry.register(metric!(
1208                name: "mz_persist_state_rollup_write_noop",
1209                help: "count of no-op rollup writes",
1210                var_labels: ["reason"],
1211        ));
1212
1213        StateMetrics {
1214            apply_spine_fast_path: registry.register(metric!(
1215                name: "mz_persist_state_apply_spine_fast_path",
1216                help: "count of spine diff applications that hit the fast path",
1217            )),
1218            apply_spine_slow_path: registry.register(metric!(
1219                name: "mz_persist_state_apply_spine_slow_path",
1220                help: "count of spine diff applications that hit the slow path",
1221            )),
1222            apply_spine_slow_path_lenient: registry.register(metric!(
1223                name: "mz_persist_state_apply_spine_slow_path_lenient",
1224                help: "count of spine diff applications that hit the lenient compaction apply path",
1225            )),
1226            apply_spine_slow_path_lenient_adjustment: registry.register(metric!(
1227                name: "mz_persist_state_apply_spine_slow_path_lenient_adjustment",
1228                help: "count of adjustments made by the lenient compaction apply path",
1229            )),
1230            apply_spine_slow_path_with_reconstruction: registry.register(metric!(
1231                name: "mz_persist_state_apply_spine_slow_path_with_reconstruction",
1232                help: "count of spine diff applications that hit the slow path with extra spine reconstruction step",
1233            )),
1234            apply_spine_flattened: registry.register(metric!(
1235                name: "mz_persist_state_apply_spine_flattened",
1236                help: "count of spine diff applications that flatten the trace",
1237            )),
1238            update_state_noop_path: registry.register(metric!(
1239                name: "mz_persist_state_update_state_noop_path",
1240                help: "count of state update applications that no-oped due to shared state",
1241            )),
1242            update_state_empty_path: registry.register(metric!(
1243                name: "mz_persist_state_update_state_empty_path",
1244                help: "count of state update applications that found no new updates",
1245            )),
1246            update_state_fast_path: registry.register(metric!(
1247                name: "mz_persist_state_update_state_fast_path",
1248                help: "count of state update applications that hit the fast path",
1249            )),
1250            update_state_slow_path: registry.register(metric!(
1251                name: "mz_persist_state_update_state_slow_path",
1252                help: "count of state update applications that hit the slow path",
1253            )),
1254            finalized_rollup_assigned: registry.register(metric!(
1255                name: "mz_persist_state_finalized_rollup_assigned",
1256                help: "count of rollups assigned on finalized shards, normally two or three per shard (steady growth means a rollup loop)",
1257            )),
1258            rollup_at_seqno_migration: registry.register(metric!(
1259                name: "mz_persist_state_rollup_at_seqno_migration",
1260                help: "count of fetch_rollup_at_seqno calls that only worked because of the migration",
1261            )),
1262            fetch_recent_live_diffs_fast_path: registry.register(metric!(
1263                name: "mz_persist_state_fetch_recent_live_diffs_fast_path",
1264                help: "count of fetch_recent_live_diffs that hit the fast path",
1265            )),
1266            fetch_recent_live_diffs_slow_path: registry.register(metric!(
1267                name: "mz_persist_state_fetch_recent_live_diffs_slow_path",
1268                help: "count of fetch_recent_live_diffs that hit the slow path",
1269            )),
1270            writer_added: registry.register(metric!(
1271                name: "mz_persist_state_writer_added",
1272                help: "count of writers added to the state",
1273            )),
1274            writer_removed: registry.register(metric!(
1275                name: "mz_persist_state_writer_removed",
1276                help: "count of writers removed from the state",
1277            )),
1278            force_apply_hostname: registry.register(metric!(
1279                name: "mz_persist_state_force_applied_hostname",
1280                help: "count of when hostname diffs needed to be force applied",
1281            )),
1282            rollup_write_success: registry.register(metric!(
1283                name: "mz_persist_state_rollup_write_success",
1284                help: "count of rollups written successful (may not be linked in to state)",
1285            )),
1286            rollup_write_noop_latest: rollup_write_noop.with_label_values(&["latest"]),
1287            rollup_write_noop_truncated: rollup_write_noop.with_label_values(&["truncated"]),
1288            hollow_run_missing_on_delete: registry.register(metric!(
1289                name: "mz_persist_state_hollow_run_missing_on_delete",
1290                help: "count of hollow run blobs found already missing while deleting the parts they reference",
1291            )),
1292        }
1293    }
1294}
1295
1296#[derive(Debug)]
1297pub struct ShardsMetrics {
1298    // Unlike all the other metrics in here, ShardsMetrics intentionally uses
1299    // the DeleteOnDrop wrappers. A process might stop using a shard (drop all
1300    // handles to it) but e.g. the set of commands never changes.
1301    //
1302    // Every per-shard family also has a bounded aggregate whose cardinality
1303    // does not grow with the shard count. `PER_SHARD_FAMILIES` lists which.
1304    //
1305    // The scrape-time aggregates, plus `mz_persist_shard_count` and
1306    // `mz_persist_stale_shard_count`, are not fields here: they live in a
1307    // `ShardsAggregateMetrics` collector that the registry owns, sharing this
1308    // struct's `shards` map so one scrape walk feeds all of them.
1309    encoded_rollup_size: mz_ore::metrics::UIntGaugeVec,
1310    encoded_diff_size: mz_ore::metrics::IntCounterVec,
1311    hollow_batch_count: mz_ore::metrics::UIntGaugeVec,
1312    spine_batch_count: mz_ore::metrics::UIntGaugeVec,
1313    batch_part_count: mz_ore::metrics::UIntGaugeVec,
1314    batch_part_version_count: mz_ore::metrics::UIntGaugeVec,
1315    update_count: mz_ore::metrics::UIntGaugeVec,
1316    rollup_count: mz_ore::metrics::UIntGaugeVec,
1317    largest_batch_size: mz_ore::metrics::UIntGaugeVec,
1318    seqnos_held: mz_ore::metrics::UIntGaugeVec,
1319    seqnos_since_last_rollup: mz_ore::metrics::UIntGaugeVec,
1320    gc_seqno_held_parts: mz_ore::metrics::UIntGaugeVec,
1321    gc_live_diffs: mz_ore::metrics::UIntGaugeVec,
1322    gc_finished: mz_ore::metrics::IntCounterVec,
1323    compaction_applied: mz_ore::metrics::IntCounterVec,
1324    cmd_succeeded: mz_ore::metrics::IntCounterVec,
1325    usage_current_state_batches_bytes: mz_ore::metrics::UIntGaugeVec,
1326    usage_current_state_rollups_bytes: mz_ore::metrics::UIntGaugeVec,
1327    usage_referenced_not_current_state_bytes: mz_ore::metrics::UIntGaugeVec,
1328    usage_not_leaked_not_referenced_bytes: mz_ore::metrics::UIntGaugeVec,
1329    usage_leaked_bytes: mz_ore::metrics::UIntGaugeVec,
1330    blob_gets: mz_ore::metrics::IntCounterVec,
1331    blob_sets: mz_ore::metrics::IntCounterVec,
1332    unconsolidated_snapshot: mz_ore::metrics::IntCounterVec,
1333    inline_part_count: UIntGaugeVec,
1334    compact_batches: UIntGaugeVec,
1335    compacting_batches: UIntGaugeVec,
1336    noncompact_batches: UIntGaugeVec,
1337    process_blob_gets: IntCounter,
1338    process_blob_sets: IntCounter,
1339    process_encoded_diff_size: IntCounter,
1340    process_unconsolidated_snapshot: IntCounter,
1341    // We hand out `Arc<ShardMetrics>` to read and write handles, but store it
1342    // here as `Weak`. This allows us to discover if it's no longer in use and
1343    // so we can remove it from the map.
1344    shards: Arc<Mutex<BTreeMap<ShardId, Weak<ShardMetrics>>>>,
1345}
1346
1347/// Removes the per-shard series this process should not export from gathered
1348/// families, as configured by [`SHARD_METRICS`] and
1349/// [`PER_SHARD_METRICS_ENABLE_REGEX`].
1350///
1351/// It matches the names in `PER_SHARD_FAMILIES`, not a name prefix, because
1352/// `mz_persist_shard_count`, `mz_persist_stale_shard_count` and the
1353/// `mz_persist_shard_*_percentile` / `_sum` / `_topk` aggregates and
1354/// `mz_persist_shard_info` share the prefix.
1355///
1356/// NOTE: Postprocessors run after collection, so the `invalid_*` counters land
1357/// in the scrape after the one that bumped them.
1358struct PerShardMetricsFilter {
1359    configs: Arc<ConfigSet>,
1360    /// The last seen value of `SHARD_METRICS` and what it parsed to.
1361    mode: (String, ShardMetricsExport),
1362    invalid_mode: IntCounter,
1363    /// The last seen value of `PER_SHARD_METRICS_ENABLE_REGEX`, and its
1364    /// compiled form if it is non-empty and valid.
1365    allow: (String, Option<Regex>),
1366    invalid_regex: IntCounter,
1367}
1368
1369impl PerShardMetricsFilter {
1370    fn filter(&mut self, families: &mut Vec<MetricFamily>) {
1371        if self.mode().per_shard() {
1372            return;
1373        }
1374        let allow = self.allow_regex();
1375        families.retain_mut(|f| {
1376            if !PER_SHARD_FAMILIES.iter().any(|p| p.name == f.name()) {
1377                return true;
1378            }
1379            let Some(allow) = allow else {
1380                return false;
1381            };
1382            f.mut_metric().retain(|m| {
1383                m.get_label()
1384                    .iter()
1385                    .any(|l| matches!(l.name(), "shard" | "name") && allow.is_match(l.value()))
1386            });
1387            !f.get_metric().is_empty()
1388        });
1389    }
1390
1391    /// Returns the regex of series to keep, or `None` to keep none.
1392    ///
1393    /// Recompiles only when the config value changes, so an invalid value is
1394    /// logged once, but it is counted on every scrape. It fails closed: an
1395    /// invalid regex keeps nothing rather than everything, which would bring
1396    /// back the full per-shard cardinality.
1397    fn allow_regex(&mut self) -> Option<&Regex> {
1398        let value = PER_SHARD_METRICS_ENABLE_REGEX.get(&self.configs);
1399        if value != self.allow.0 {
1400            let compiled = if value.is_empty() {
1401                None
1402            } else {
1403                Regex::new(&value)
1404                    .inspect_err(|err| {
1405                        warn!(
1406                            "invalid {}, keeping no per-shard series: {err}",
1407                            PER_SHARD_METRICS_ENABLE_REGEX.name()
1408                        )
1409                    })
1410                    .ok()
1411            };
1412            self.allow = (value, compiled);
1413        }
1414        if !self.allow.0.is_empty() && self.allow.1.is_none() {
1415            self.invalid_regex.inc();
1416        }
1417        self.allow.1.as_ref()
1418    }
1419
1420    /// The export mode, or the default if the config value is unrecognized.
1421    ///
1422    /// Reparses only on change, so an unrecognized value logs once but counts
1423    /// every scrape.
1424    fn mode(&mut self) -> ShardMetricsExport {
1425        let value = SHARD_METRICS.get(&self.configs);
1426        if value != self.mode.0 {
1427            let parsed = ShardMetricsExport::parse(&value).unwrap_or_else(|| {
1428                let default = ShardMetricsExport::default();
1429                warn!(
1430                    "unrecognized {}: '{value}', exporting '{}'",
1431                    SHARD_METRICS.name(),
1432                    default.as_str()
1433                );
1434                default
1435            });
1436            self.mode = (value, parsed);
1437        }
1438        if self.mode.1.as_str() != self.mode.0 {
1439            self.invalid_mode.inc();
1440        }
1441        self.mode.1
1442    }
1443}
1444
1445impl ShardsMetrics {
1446    fn new(registry: &MetricsRegistry, configs: Arc<ConfigSet>) -> Self {
1447        let shards = Arc::new(Mutex::new(BTreeMap::new()));
1448        registry.register_collector(ShardsAggregateMetrics::new(Arc::clone(&shards), configs));
1449        ShardsMetrics {
1450            encoded_rollup_size: registry.register(metric!(
1451                name: "mz_persist_shard_rollup_size_bytes",
1452                help: "total encoded rollup size by shard",
1453                var_labels: ["shard", "name"],
1454            )),
1455            encoded_diff_size: registry.register(metric!(
1456                name: "mz_persist_shard_diff_size_bytes",
1457                help: "total encoded diff size by shard",
1458                var_labels: ["shard", "name"],
1459            )),
1460            hollow_batch_count: registry.register(metric!(
1461                name: "mz_persist_shard_hollow_batch_count",
1462                help: "count of hollow batches by shard",
1463                var_labels: ["shard", "name"],
1464            )),
1465            spine_batch_count: registry.register(metric!(
1466                name: "mz_persist_shard_spine_batch_count",
1467                help: "count of spine batches by shard",
1468                var_labels: ["shard", "name"],
1469            )),
1470            batch_part_count: registry.register(metric!(
1471                name: "mz_persist_shard_batch_part_count",
1472                help: "count of batch parts by shard",
1473                var_labels: ["shard", "name"],
1474            )),
1475            batch_part_version_count: registry.register(metric!(
1476                name: "mz_persist_shard_batch_part_version_count",
1477                help: "count of batch parts by shard and version",
1478                var_labels: ["shard", "name", "version"],
1479            )),
1480            update_count: registry.register(metric!(
1481                name: "mz_persist_shard_update_count",
1482                help: "count of updates by shard",
1483                var_labels: ["shard", "name"],
1484            )),
1485            rollup_count: registry.register(metric!(
1486                name: "mz_persist_shard_rollup_count",
1487                help: "count of rollups by shard",
1488                var_labels: ["shard", "name"],
1489            )),
1490            largest_batch_size: registry.register(metric!(
1491                name: "mz_persist_shard_largest_batch_size",
1492                help: "largest encoded batch size by shard",
1493                var_labels: ["shard", "name"],
1494            )),
1495            seqnos_held: registry.register(metric!(
1496                name: "mz_persist_shard_seqnos_held",
1497                help: "maximum count of gc-ineligible states by shard",
1498                var_labels: ["shard", "name"],
1499            )),
1500            seqnos_since_last_rollup: registry.register(metric!(
1501                name: "mz_persist_shard_seqnos_since_last_rollup",
1502                help: "count of seqnos since last rollup",
1503                var_labels: ["shard", "name"],
1504            )),
1505            gc_seqno_held_parts: registry.register(metric!(
1506                name: "mz_persist_shard_gc_seqno_held_parts",
1507                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",
1508                var_labels: ["shard", "name"],
1509            )),
1510            gc_live_diffs: registry.register(metric!(
1511                name: "mz_persist_shard_gc_live_diffs",
1512                help: "the number of diffs (or, alternatively, the number of seqnos) present in consensus state at GC time",
1513                var_labels: ["shard", "name"],
1514            )),
1515            gc_finished: registry.register(metric!(
1516                name: "mz_persist_shard_gc_finished",
1517                help: "count of garbage collections finished by shard",
1518                var_labels: ["shard", "name"],
1519            )),
1520            compaction_applied: registry.register(metric!(
1521                name: "mz_persist_shard_compaction_applied",
1522                help: "count of compactions applied to state by shard",
1523                var_labels: ["shard", "name"],
1524            )),
1525            cmd_succeeded: registry.register(metric!(
1526                name: "mz_persist_shard_cmd_succeeded",
1527                help: "count of commands succeeded by shard",
1528                var_labels: ["shard", "name"],
1529            )),
1530            usage_current_state_batches_bytes: registry.register(metric!(
1531                name: "mz_persist_shard_usage_current_state_batches_bytes",
1532                help: "data in batches/parts referenced by current version of state",
1533                var_labels: ["shard", "name"],
1534            )),
1535            usage_current_state_rollups_bytes: registry.register(metric!(
1536                name: "mz_persist_shard_usage_current_state_rollups_bytes",
1537                help: "data in rollups referenced by current version of state",
1538                var_labels: ["shard", "name"],
1539            )),
1540            usage_referenced_not_current_state_bytes: registry.register(metric!(
1541                name: "mz_persist_shard_usage_referenced_not_current_state_bytes",
1542                help: "data referenced only by a previous version of state",
1543                var_labels: ["shard", "name"],
1544            )),
1545            usage_not_leaked_not_referenced_bytes: registry.register(metric!(
1546                name: "mz_persist_shard_usage_not_leaked_not_referenced_bytes",
1547                help: "data written by an active writer but not referenced by any version of state",
1548                var_labels: ["shard", "name"],
1549            )),
1550            usage_leaked_bytes: registry.register(metric!(
1551                name: "mz_persist_shard_usage_leaked_bytes",
1552                help: "data reclaimable by a leaked blob detector",
1553                var_labels: ["shard", "name"],
1554            )),
1555            blob_gets: registry.register(metric!(
1556                name: "mz_persist_shard_blob_gets",
1557                help: "number of Blob::get calls for this shard",
1558                var_labels: ["shard", "name"],
1559            )),
1560            blob_sets: registry.register(metric!(
1561                name: "mz_persist_shard_blob_sets",
1562                help: "number of Blob::set calls for this shard",
1563                var_labels: ["shard", "name"],
1564            )),
1565            unconsolidated_snapshot: registry.register(metric!(
1566                name: "mz_persist_shard_unconsolidated_snapshot",
1567                help: "in snapshot_and_read, the number of times consolidating the raw data wasn't enough to produce consolidated output",
1568                var_labels: ["shard", "name"],
1569            )),
1570            inline_part_count: registry.register(metric!(
1571                name: "mz_persist_shard_inline_part_count",
1572                help: "count of parts inline in shard metadata",
1573                var_labels: ["shard", "name"],
1574            )),
1575            compact_batches: registry.register(metric!(
1576                name: "mz_persist_shard_compact_batches",
1577                help: "number of fully compact batches in the shard",
1578                var_labels: ["shard", "name"],
1579            )),
1580            compacting_batches: registry.register(metric!(
1581                name: "mz_persist_shard_compacting_batches",
1582                help: "number of batches in the shard with compactions in progress",
1583                var_labels: ["shard", "name"],
1584            )),
1585            noncompact_batches: registry.register(metric!(
1586                name: "mz_persist_shard_noncompact_batches",
1587                help: "number of batches in the shard that aren't compact and have no ongoing compaction",
1588                var_labels: ["shard", "name"],
1589            )),
1590            process_blob_gets: registry.register(metric!(
1591                name: "mz_persist_blob_gets",
1592                help: "number of Blob::get calls for batch parts, over all shards",
1593            )),
1594            process_blob_sets: registry.register(metric!(
1595                name: "mz_persist_blob_sets",
1596                help: "number of Blob::set calls for batch parts, over all shards",
1597            )),
1598            process_encoded_diff_size: registry.register(metric!(
1599                name: "mz_persist_diff_size_bytes",
1600                help: "total encoded diff size, over all shards",
1601            )),
1602            process_unconsolidated_snapshot: registry.register(metric!(
1603                name: "mz_persist_unconsolidated_snapshot",
1604                help: "in snapshot_and_read, the number of times consolidating the raw data wasn't enough to produce consolidated output, over all shards",
1605            )),
1606            shards,
1607        }
1608    }
1609
1610    pub fn shard(&self, shard_id: &ShardId, name: &str) -> Arc<ShardMetrics> {
1611        let mut shards = self.shards.lock().expect("mutex poisoned");
1612        if let Some(shard) = shards.get(shard_id) {
1613            if let Some(shard) = shard.upgrade() {
1614                return Arc::clone(&shard);
1615            } else {
1616                assert!(shards.remove(shard_id).is_some());
1617            }
1618        }
1619        let shard = Arc::new(ShardMetrics::new(shard_id, name, self));
1620        assert!(
1621            shards
1622                .insert(shard_id.clone(), Arc::downgrade(&shard))
1623                .is_none()
1624        );
1625        shard
1626    }
1627
1628    /// Returns the shards that still have handles, and forgets the rest.
1629    ///
1630    /// Holds the `shards` lock, which `Self::shard` needs, so it does nothing
1631    /// but upgrade each entry.
1632    fn live_shards(
1633        shards: &Arc<Mutex<BTreeMap<ShardId, Weak<ShardMetrics>>>>,
1634    ) -> Vec<Arc<ShardMetrics>> {
1635        let mut shards = shards.lock().expect("mutex poisoned");
1636        let mut live = Vec::with_capacity(shards.len());
1637        shards.retain(|_, metrics| match metrics.upgrade() {
1638            Some(metrics) => {
1639                live.push(metrics);
1640                true
1641            }
1642            None => false,
1643        });
1644        live
1645    }
1646}
1647
1648/// A family that [`ShardsMetrics::new`] registers with one series per shard,
1649/// and the bounded aggregate that covers it.
1650struct PerShardFamily {
1651    name: &'static str,
1652    aggregate: ShardAggregate,
1653}
1654
1655enum ShardAggregate {
1656    /// Folded into `<name>_percentile` and `<name>_topk` at scrape time.
1657    Gauge(ShardGauge),
1658    /// Summed over shards into `mz_persist_batch_part_version_count`.
1659    VersionSum,
1660    /// Counted in full by the named process-level counter.
1661    ///
1662    /// NOTE: For `gc_finished`, `compaction_applied` and `cmd_succeeded` the
1663    /// process counter is kept equal to the sum over shards by a paired
1664    /// `.inc()` at each call site, not by construction like [`ShardCounter`].
1665    /// Add one without the other and the total drifts, with no test to catch it.
1666    Counter {
1667        // Only read by tests: that it names a registered counter, and that
1668        // every export mode keeps that counter.
1669        #[allow(dead_code)]
1670        total: &'static str,
1671    },
1672}
1673
1674struct ShardGauge {
1675    /// The registered help of the per-shard family, which the aggregate
1676    /// extends. `per_shard_families_match_registrations` checks they match.
1677    help: &'static str,
1678    /// How many of the largest shards `<name>_topk` names.
1679    top_k: usize,
1680    /// Whether to emit `<name>_sum`. See [`AggregatedFamily::sum`].
1681    sum: bool,
1682    get: fn(&ShardMetrics) -> u64,
1683}
1684
1685/// The default `ShardGauge::top_k`.
1686const SHARD_TOP_K: usize = 10;
1687
1688/// Every per-shard family. Must list exactly the families with a `shard`
1689/// label that `ShardsMetrics::new` registers.
1690const PER_SHARD_FAMILIES: &[PerShardFamily] = &[
1691    PerShardFamily {
1692        name: "mz_persist_shard_rollup_size_bytes",
1693        aggregate: ShardAggregate::Gauge(ShardGauge {
1694            help: "total encoded rollup size by shard",
1695            top_k: SHARD_TOP_K,
1696            sum: true,
1697            get: |m| m.latest_rollup_size.get(),
1698        }),
1699    },
1700    PerShardFamily {
1701        name: "mz_persist_shard_diff_size_bytes",
1702        aggregate: ShardAggregate::Counter {
1703            total: "mz_persist_diff_size_bytes",
1704        },
1705    },
1706    PerShardFamily {
1707        name: "mz_persist_shard_hollow_batch_count",
1708        aggregate: ShardAggregate::Gauge(ShardGauge {
1709            help: "count of hollow batches by shard",
1710            top_k: SHARD_TOP_K,
1711            sum: true,
1712            get: |m| m.hollow_batch_count.get(),
1713        }),
1714    },
1715    PerShardFamily {
1716        name: "mz_persist_shard_spine_batch_count",
1717        aggregate: ShardAggregate::Gauge(ShardGauge {
1718            help: "count of spine batches by shard",
1719            top_k: SHARD_TOP_K,
1720            sum: true,
1721            get: |m| m.spine_batch_count.get(),
1722        }),
1723    },
1724    PerShardFamily {
1725        name: "mz_persist_shard_batch_part_count",
1726        aggregate: ShardAggregate::Gauge(ShardGauge {
1727            help: "count of batch parts by shard",
1728            top_k: SHARD_TOP_K,
1729            sum: true,
1730            get: |m| m.batch_part_count.get(),
1731        }),
1732    },
1733    PerShardFamily {
1734        name: "mz_persist_shard_batch_part_version_count",
1735        aggregate: ShardAggregate::VersionSum,
1736    },
1737    PerShardFamily {
1738        name: "mz_persist_shard_update_count",
1739        aggregate: ShardAggregate::Gauge(ShardGauge {
1740            help: "count of updates by shard",
1741            top_k: SHARD_TOP_K,
1742            sum: true,
1743            get: |m| m.update_count.get(),
1744        }),
1745    },
1746    PerShardFamily {
1747        name: "mz_persist_shard_rollup_count",
1748        aggregate: ShardAggregate::Gauge(ShardGauge {
1749            help: "count of rollups by shard",
1750            top_k: SHARD_TOP_K,
1751            sum: true,
1752            get: |m| m.rollup_count.get(),
1753        }),
1754    },
1755    PerShardFamily {
1756        name: "mz_persist_shard_largest_batch_size",
1757        aggregate: ShardAggregate::Gauge(ShardGauge {
1758            help: "largest encoded batch size by shard",
1759            top_k: SHARD_TOP_K,
1760            sum: false,
1761            get: |m| m.largest_batch_size.get(),
1762        }),
1763    },
1764    PerShardFamily {
1765        name: "mz_persist_shard_seqnos_held",
1766        aggregate: ShardAggregate::Gauge(ShardGauge {
1767            help: "maximum count of gc-ineligible states by shard",
1768            top_k: SHARD_TOP_K,
1769            sum: true,
1770            get: |m| m.seqnos_held.get(),
1771        }),
1772    },
1773    PerShardFamily {
1774        name: "mz_persist_shard_seqnos_since_last_rollup",
1775        aggregate: ShardAggregate::Gauge(ShardGauge {
1776            help: "count of seqnos since last rollup",
1777            top_k: SHARD_TOP_K,
1778            sum: true,
1779            get: |m| m.seqnos_since_last_rollup.get(),
1780        }),
1781    },
1782    PerShardFamily {
1783        name: "mz_persist_shard_gc_seqno_held_parts",
1784        aggregate: ShardAggregate::Gauge(ShardGauge {
1785            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",
1786            top_k: SHARD_TOP_K,
1787            sum: true,
1788            get: |m| m.gc_seqno_held_parts.get(),
1789        }),
1790    },
1791    PerShardFamily {
1792        name: "mz_persist_shard_gc_live_diffs",
1793        aggregate: ShardAggregate::Gauge(ShardGauge {
1794            help: "the number of diffs (or, alternatively, the number of seqnos) present in consensus state at GC time",
1795            top_k: SHARD_TOP_K,
1796            sum: true,
1797            get: |m| m.gc_live_diffs.get(),
1798        }),
1799    },
1800    PerShardFamily {
1801        name: "mz_persist_shard_gc_finished",
1802        aggregate: ShardAggregate::Counter {
1803            total: "mz_persist_gc_finished",
1804        },
1805    },
1806    PerShardFamily {
1807        name: "mz_persist_shard_compaction_applied",
1808        aggregate: ShardAggregate::Counter {
1809            total: "mz_persist_compaction_applied",
1810        },
1811    },
1812    PerShardFamily {
1813        name: "mz_persist_shard_cmd_succeeded",
1814        aggregate: ShardAggregate::Counter {
1815            total: "mz_persist_cmd_succeeded_count",
1816        },
1817    },
1818    PerShardFamily {
1819        name: "mz_persist_shard_usage_current_state_batches_bytes",
1820        aggregate: ShardAggregate::Gauge(ShardGauge {
1821            help: "data in batches/parts referenced by current version of state",
1822            top_k: SHARD_TOP_K,
1823            sum: true,
1824            get: |m| m.usage_current_state_batches_bytes.get(),
1825        }),
1826    },
1827    PerShardFamily {
1828        name: "mz_persist_shard_usage_current_state_rollups_bytes",
1829        aggregate: ShardAggregate::Gauge(ShardGauge {
1830            help: "data in rollups referenced by current version of state",
1831            top_k: SHARD_TOP_K,
1832            sum: true,
1833            get: |m| m.usage_current_state_rollups_bytes.get(),
1834        }),
1835    },
1836    PerShardFamily {
1837        name: "mz_persist_shard_usage_referenced_not_current_state_bytes",
1838        aggregate: ShardAggregate::Gauge(ShardGauge {
1839            help: "data referenced only by a previous version of state",
1840            top_k: SHARD_TOP_K,
1841            sum: true,
1842            get: |m| m.usage_referenced_not_current_state_bytes.get(),
1843        }),
1844    },
1845    PerShardFamily {
1846        name: "mz_persist_shard_usage_not_leaked_not_referenced_bytes",
1847        aggregate: ShardAggregate::Gauge(ShardGauge {
1848            help: "data written by an active writer but not referenced by any version of state",
1849            top_k: SHARD_TOP_K,
1850            sum: true,
1851            get: |m| m.usage_not_leaked_not_referenced_bytes.get(),
1852        }),
1853    },
1854    PerShardFamily {
1855        name: "mz_persist_shard_usage_leaked_bytes",
1856        aggregate: ShardAggregate::Gauge(ShardGauge {
1857            help: "data reclaimable by a leaked blob detector",
1858            top_k: SHARD_TOP_K,
1859            sum: true,
1860            get: |m| m.usage_leaked_bytes.get(),
1861        }),
1862    },
1863    PerShardFamily {
1864        name: "mz_persist_shard_blob_gets",
1865        aggregate: ShardAggregate::Counter {
1866            total: "mz_persist_blob_gets",
1867        },
1868    },
1869    PerShardFamily {
1870        name: "mz_persist_shard_blob_sets",
1871        aggregate: ShardAggregate::Counter {
1872            total: "mz_persist_blob_sets",
1873        },
1874    },
1875    PerShardFamily {
1876        name: "mz_persist_shard_unconsolidated_snapshot",
1877        aggregate: ShardAggregate::Counter {
1878            total: "mz_persist_unconsolidated_snapshot",
1879        },
1880    },
1881    PerShardFamily {
1882        name: "mz_persist_shard_inline_part_count",
1883        aggregate: ShardAggregate::Gauge(ShardGauge {
1884            help: "count of parts inline in shard metadata",
1885            top_k: SHARD_TOP_K,
1886            sum: true,
1887            get: |m| m.inline_part_count.get(),
1888        }),
1889    },
1890    PerShardFamily {
1891        name: "mz_persist_shard_compact_batches",
1892        aggregate: ShardAggregate::Gauge(ShardGauge {
1893            help: "number of fully compact batches in the shard",
1894            top_k: SHARD_TOP_K,
1895            sum: true,
1896            get: |m| m.compact_batches.get(),
1897        }),
1898    },
1899    PerShardFamily {
1900        name: "mz_persist_shard_compacting_batches",
1901        aggregate: ShardAggregate::Gauge(ShardGauge {
1902            help: "number of batches in the shard with compactions in progress",
1903            top_k: SHARD_TOP_K,
1904            sum: true,
1905            get: |m| m.compacting_batches.get(),
1906        }),
1907    },
1908    PerShardFamily {
1909        name: "mz_persist_shard_noncompact_batches",
1910        aggregate: ShardAggregate::Gauge(ShardGauge {
1911            help: "number of batches in the shard that aren't compact and have no ongoing compaction",
1912            top_k: SHARD_TOP_K,
1913            sum: true,
1914            get: |m| m.noncompact_batches.get(),
1915        }),
1916    },
1917];
1918
1919/// The per-shard gauges and their names, in `PER_SHARD_FAMILIES` order.
1920fn shard_gauges() -> impl Iterator<Item = (&'static str, &'static ShardGauge)> {
1921    PER_SHARD_FAMILIES
1922        .iter()
1923        .filter_map(|f| match &f.aggregate {
1924            ShardAggregate::Gauge(g) => Some((f.name, g)),
1925            _ => None,
1926        })
1927}
1928
1929const SHARD_INFO_NAME: &str = "mz_persist_shard_info";
1930const BATCH_PART_VERSION_COUNT_NAME: &str = "mz_persist_batch_part_version_count";
1931const BATCH_PART_VERSION_COUNT_HELP: &str = "count of batch parts by version, over all shards";
1932
1933/// Identifies a shard in the `_topk` families, with the same labels as the
1934/// per-shard series.
1935#[derive(Debug)]
1936struct ShardKey(Arc<ShardMetrics>);
1937
1938impl AggregationKey for ShardKey {
1939    const LABEL_NAMES: &'static [&'static str] = &["shard", "name"];
1940
1941    fn label_values(&self) -> Vec<String> {
1942        vec![self.0.shard_id.to_string(), self.0.name.clone()]
1943    }
1944}
1945
1946/// Process-level metrics derived from the shards map, collected in a single
1947/// walk: the shard counts, percentiles, a top-K and (where meaningful) a sum
1948/// per per-shard gauge, the per-version batch part count summed over shards,
1949/// and, if [`SHARD_INFO_METRIC`] is set, `mz_persist_shard_info`.
1950///
1951/// All but `mz_persist_shard_info` have a cardinality independent of the
1952/// number of shards.
1953#[derive(Debug)]
1954struct ShardsAggregateMetrics {
1955    shards: Arc<Mutex<BTreeMap<ShardId, Weak<ShardMetrics>>>>,
1956    configs: Arc<ConfigSet>,
1957    count: GenericGauge<AtomicI64>,
1958    stale_count: GenericGauge<AtomicI64>,
1959    gauges: AggregatedFamilies<ShardKey>,
1960    /// The getters behind `gauges`, same order, so the per-shard walk reads a
1961    /// slice instead of re-deriving them per shard.
1962    gauge_gets: Vec<fn(&ShardMetrics) -> u64>,
1963    // Only used for `desc`. `collect` builds a fresh vec each scrape so that
1964    // versions no shard has anymore are not exported.
1965    batch_part_version_count: raw::UIntGaugeVec,
1966    // Only used for `desc`, like `batch_part_version_count`.
1967    shard_info: raw::UIntGaugeVec,
1968}
1969
1970impl ShardsAggregateMetrics {
1971    fn new(
1972        shards: Arc<Mutex<BTreeMap<ShardId, Weak<ShardMetrics>>>>,
1973        configs: Arc<ConfigSet>,
1974    ) -> Self {
1975        let gauges = shard_gauges()
1976            .map(|(name, g)| AggregatedFamily {
1977                name: name.to_string(),
1978                help: g.help.to_string(),
1979                sum: g.sum,
1980                top_k: Some(g.top_k),
1981            })
1982            .collect();
1983        ShardsAggregateMetrics {
1984            shards,
1985            configs,
1986            count: MakeCollector::make_collector(metric!(
1987                name: "mz_persist_shard_count",
1988                help: "count of all active shards on this process",
1989            )),
1990            stale_count: MakeCollector::make_collector(metric!(
1991                name: "mz_persist_stale_shard_count",
1992                help: "count of shards on this process whose persisted state version \
1993                       is behind this process's build version; per-process, so summing \
1994                       across processes counts (shard, process) pairs, not distinct shards",
1995            )),
1996            gauges: AggregatedFamilies::new(gauges),
1997            gauge_gets: shard_gauges().map(|(_, g)| g.get).collect(),
1998            batch_part_version_count: new_batch_part_version_count(),
1999            shard_info: new_shard_info(),
2000        }
2001    }
2002
2003    /// The bounded aggregates of the per-shard families over `live`.
2004    fn summaries(&self, live: Vec<Arc<ShardMetrics>>) -> Vec<MetricFamily> {
2005        let mut versions = BTreeMap::<String, u64>::new();
2006        let mut values = Vec::new();
2007        let mut snapshot = self.gauges.snapshot();
2008        for m in live {
2009            values.clear();
2010            values.extend(self.gauge_gets.iter().map(|get| get(&m)));
2011            let version_map = m
2012                .batch_part_version_map
2013                .lock()
2014                .expect("mutex should not be poisoned");
2015            for (version, metrics) in version_map.iter() {
2016                let n = metrics.batch_part_version_count.get();
2017                if n == 0 {
2018                    continue;
2019                }
2020                match versions.get_mut(version) {
2021                    Some(total) => *total += n,
2022                    None => {
2023                        versions.insert(version.clone(), n);
2024                    }
2025                }
2026            }
2027            drop(version_map);
2028            snapshot.push(ShardKey(m), &values);
2029        }
2030
2031        let mut families = snapshot.finish();
2032        let batch_part_version_count = new_batch_part_version_count();
2033        for (version, n) in versions {
2034            batch_part_version_count
2035                .with_label_values(&[version.as_str()])
2036                .set(n);
2037        }
2038        families.extend(batch_part_version_count.collect());
2039        families
2040    }
2041}
2042
2043/// Lists the shards, which the `_topk` families name only the largest of. One
2044/// series per shard, so unlike the summaries it grows with the shard count.
2045fn new_shard_info() -> raw::UIntGaugeVec {
2046    raw::UIntGaugeVec::new(
2047        PrometheusOpts::new(SHARD_INFO_NAME, "1 for each active shard on this process"),
2048        ShardKey::LABEL_NAMES,
2049    )
2050    .expect("valid gauge vec")
2051}
2052
2053fn new_batch_part_version_count() -> raw::UIntGaugeVec {
2054    raw::UIntGaugeVec::new(
2055        PrometheusOpts::new(BATCH_PART_VERSION_COUNT_NAME, BATCH_PART_VERSION_COUNT_HELP),
2056        &["version"],
2057    )
2058    .expect("valid gauge vec")
2059}
2060
2061impl Collector for ShardsAggregateMetrics {
2062    fn desc(&self) -> Vec<&Desc> {
2063        let mut descs = self.count.desc();
2064        descs.extend(self.stale_count.desc());
2065        descs.extend(self.gauges.descs());
2066        descs.extend(self.batch_part_version_count.desc());
2067        descs.extend(self.shard_info.desc());
2068        descs
2069    }
2070
2071    fn collect(&self) -> Vec<MetricFamily> {
2072        let live = ShardsMetrics::live_shards(&self.shards);
2073        let mut count = 0;
2074        let mut stale_count = 0;
2075        for m in &live {
2076            count += 1;
2077            if m.stale.load(Ordering::Relaxed) {
2078                stale_count += 1;
2079            }
2080        }
2081        self.count.set(count);
2082        self.stale_count.set(stale_count);
2083
2084        let mut families = Vec::new();
2085        if SHARD_INFO_METRIC.get(&self.configs) {
2086            let shard_info = new_shard_info();
2087            for m in &live {
2088                shard_info
2089                    .with_label_values(&[m.shard_id.to_string(), m.name.clone()])
2090                    .set(1);
2091            }
2092            families.extend(shard_info.collect());
2093        }
2094        if ShardMetricsExport::get(&self.configs).summary() {
2095            families.extend(self.summaries(live));
2096        }
2097        families.extend(self.count.collect());
2098        families.extend(self.stale_count.collect());
2099        families
2100    }
2101}
2102
2103#[derive(Debug)]
2104pub struct ShardMetrics {
2105    pub shard_id: ShardId,
2106    pub name: String,
2107    pub largest_batch_size: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2108    pub latest_rollup_size: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2109    pub encoded_diff_size: ShardCounter,
2110    pub hollow_batch_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2111    pub spine_batch_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2112    pub batch_part_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2113    batch_part_version_count: mz_ore::metrics::UIntGaugeVec,
2114    batch_part_version_map: Mutex<BTreeMap<String, BatchPartVersionMetrics>>,
2115    pub update_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2116    pub rollup_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2117    pub seqnos_held: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2118    pub seqnos_since_last_rollup: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2119    pub gc_seqno_held_parts: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2120    pub gc_live_diffs: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2121    pub usage_current_state_batches_bytes: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2122    pub usage_current_state_rollups_bytes: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2123    pub usage_referenced_not_current_state_bytes: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2124    pub usage_not_leaked_not_referenced_bytes: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2125    pub usage_leaked_bytes: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2126    pub gc_finished: DeleteOnDropCounter<AtomicU64, Vec<String>>,
2127    pub compaction_applied: DeleteOnDropCounter<AtomicU64, Vec<String>>,
2128    pub cmd_succeeded: DeleteOnDropCounter<AtomicU64, Vec<String>>,
2129    pub blob_gets: ShardCounter,
2130    pub blob_sets: ShardCounter,
2131    pub unconsolidated_snapshot: ShardCounter,
2132    pub inline_part_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2133    pub compact_batches: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2134    pub compacting_batches: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2135    pub noncompact_batches: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2136    // Not a registered per-shard series: this feeds the process-level
2137    // `mz_persist_stale_shard_count` computed gauge, which sums it across shards
2138    // instead of exporting one series per shard.
2139    pub stale: AtomicBool,
2140}
2141
2142impl ShardMetrics {
2143    pub fn new(shard_id: &ShardId, name: &str, shards_metrics: &ShardsMetrics) -> Self {
2144        let shard = shard_id.to_string();
2145        ShardMetrics {
2146            shard_id: *shard_id,
2147            name: name.to_string(),
2148            latest_rollup_size: shards_metrics
2149                .encoded_rollup_size
2150                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2151            encoded_diff_size: ShardCounter {
2152                shard: shards_metrics
2153                    .encoded_diff_size
2154                    .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2155                process: shards_metrics.process_encoded_diff_size.clone(),
2156            },
2157            hollow_batch_count: shards_metrics
2158                .hollow_batch_count
2159                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2160            spine_batch_count: shards_metrics
2161                .spine_batch_count
2162                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2163            batch_part_count: shards_metrics
2164                .batch_part_count
2165                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2166            batch_part_version_count: shards_metrics.batch_part_version_count.clone(),
2167            batch_part_version_map: Mutex::new(BTreeMap::new()),
2168            update_count: shards_metrics
2169                .update_count
2170                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2171            rollup_count: shards_metrics
2172                .rollup_count
2173                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2174            largest_batch_size: shards_metrics
2175                .largest_batch_size
2176                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2177            seqnos_held: shards_metrics
2178                .seqnos_held
2179                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2180            seqnos_since_last_rollup: shards_metrics
2181                .seqnos_since_last_rollup
2182                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2183            gc_seqno_held_parts: shards_metrics
2184                .gc_seqno_held_parts
2185                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2186            gc_live_diffs: shards_metrics
2187                .gc_live_diffs
2188                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2189            gc_finished: shards_metrics
2190                .gc_finished
2191                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2192            compaction_applied: shards_metrics
2193                .compaction_applied
2194                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2195            cmd_succeeded: shards_metrics
2196                .cmd_succeeded
2197                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2198            usage_current_state_batches_bytes: shards_metrics
2199                .usage_current_state_batches_bytes
2200                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2201            usage_current_state_rollups_bytes: shards_metrics
2202                .usage_current_state_rollups_bytes
2203                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2204            usage_referenced_not_current_state_bytes: shards_metrics
2205                .usage_referenced_not_current_state_bytes
2206                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2207            usage_not_leaked_not_referenced_bytes: shards_metrics
2208                .usage_not_leaked_not_referenced_bytes
2209                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2210            usage_leaked_bytes: shards_metrics
2211                .usage_leaked_bytes
2212                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2213            blob_gets: ShardCounter {
2214                shard: shards_metrics
2215                    .blob_gets
2216                    .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2217                process: shards_metrics.process_blob_gets.clone(),
2218            },
2219            blob_sets: ShardCounter {
2220                shard: shards_metrics
2221                    .blob_sets
2222                    .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2223                process: shards_metrics.process_blob_sets.clone(),
2224            },
2225            unconsolidated_snapshot: ShardCounter {
2226                shard: shards_metrics
2227                    .unconsolidated_snapshot
2228                    .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2229                process: shards_metrics.process_unconsolidated_snapshot.clone(),
2230            },
2231            inline_part_count: shards_metrics
2232                .inline_part_count
2233                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2234            compact_batches: shards_metrics
2235                .compact_batches
2236                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2237            compacting_batches: shards_metrics
2238                .compacting_batches
2239                .get_delete_on_drop_metric(vec![shard.clone(), name.to_string()]),
2240            noncompact_batches: shards_metrics
2241                .noncompact_batches
2242                .get_delete_on_drop_metric(vec![shard, name.to_string()]),
2243            stale: AtomicBool::new(false),
2244        }
2245    }
2246
2247    pub(crate) fn set_batch_part_versions<'a>(
2248        &self,
2249        batch_parts_by_version: impl Iterator<Item = &'a str>,
2250    ) {
2251        let mut map = self
2252            .batch_part_version_map
2253            .lock()
2254            .expect("mutex should not be poisoned");
2255        // NB: It's a bit sus that the below assumes that no one else is
2256        // concurrently modifying the atomics in the gauges, but we're holding
2257        // the mutex this whole time, so it should be true.
2258
2259        // We want to do this in a way that avoids allocating (e.g. summing up a
2260        // map). First reset everything.
2261        for x in map.values() {
2262            x.batch_part_version_count.set(0);
2263        }
2264
2265        // Then go through the iterator, creating new entries as necessary and
2266        // adding.
2267        for key in batch_parts_by_version {
2268            if !map.contains_key(key) {
2269                map.insert(
2270                    key.to_owned(),
2271                    BatchPartVersionMetrics {
2272                        batch_part_version_count: self
2273                            .batch_part_version_count
2274                            .get_delete_on_drop_metric(vec![
2275                                self.shard_id.to_string(),
2276                                self.name.clone(),
2277                                key.to_owned(),
2278                            ]),
2279                    },
2280                );
2281            }
2282            let value = map.get(key).expect("inserted above");
2283            value.batch_part_version_count.inc();
2284        }
2285    }
2286}
2287
2288/// A per-shard counter that also bumps a process-level total, so the total
2289/// stays monotonic when shards are dropped.
2290#[derive(Debug)]
2291pub struct ShardCounter {
2292    shard: DeleteOnDropCounter<AtomicU64, Vec<String>>,
2293    process: IntCounter,
2294}
2295
2296impl ShardCounter {
2297    pub fn inc(&self) {
2298        self.inc_by(1);
2299    }
2300
2301    pub fn inc_by(&self, v: u64) {
2302        self.shard.inc_by(v);
2303        self.process.inc_by(v);
2304    }
2305}
2306
2307#[derive(Debug)]
2308pub struct BatchPartVersionMetrics {
2309    pub batch_part_version_count: DeleteOnDropGauge<AtomicU64, Vec<String>>,
2310}
2311
2312/// Metrics recorded by audits of persist usage
2313#[derive(Debug)]
2314pub struct UsageAuditMetrics {
2315    /// Size of all batch parts stored in Blob
2316    pub blob_batch_part_bytes: UIntGauge,
2317    /// Count of batch parts stored in Blob
2318    pub blob_batch_part_count: UIntGauge,
2319    /// Size of all state rollups stored in Blob
2320    pub blob_rollup_bytes: UIntGauge,
2321    /// Count of state rollups stored in Blob
2322    pub blob_rollup_count: UIntGauge,
2323    /// Size of Blob
2324    pub blob_bytes: UIntGauge,
2325    /// Count of all blobs
2326    pub blob_count: UIntGauge,
2327    /// Time spent fetching blob metadata
2328    pub step_blob_metadata: Counter,
2329    /// Time spent fetching state versions
2330    pub step_state: Counter,
2331    /// Time spent doing math
2332    pub step_math: Counter,
2333}
2334
2335impl UsageAuditMetrics {
2336    fn new(registry: &MetricsRegistry) -> Self {
2337        let step_timings: CounterVec = registry.register(metric!(
2338                name: "mz_persist_audit_step_seconds",
2339                help: "time spent on individual steps of audit",
2340                var_labels: ["step"],
2341        ));
2342        UsageAuditMetrics {
2343            blob_batch_part_bytes: registry.register(metric!(
2344                name: "mz_persist_audit_blob_batch_part_bytes",
2345                help: "total size of batch parts in blob",
2346            )),
2347            blob_batch_part_count: registry.register(metric!(
2348                name: "mz_persist_audit_blob_batch_part_count",
2349                help: "count of batch parts in blob",
2350            )),
2351            blob_rollup_bytes: registry.register(metric!(
2352                name: "mz_persist_audit_blob_rollup_bytes",
2353                help: "total size of state rollups stored in blob",
2354            )),
2355            blob_rollup_count: registry.register(metric!(
2356                name: "mz_persist_audit_blob_rollup_count",
2357                help: "count of all state rollups in blob",
2358            )),
2359            blob_bytes: registry.register(metric!(
2360                name: "mz_persist_audit_blob_bytes",
2361                help: "total size of blob",
2362            )),
2363            blob_count: registry.register(metric!(
2364                name: "mz_persist_audit_blob_count",
2365                help: "count of all blobs",
2366            )),
2367            step_blob_metadata: step_timings.with_label_values(&["blob_metadata"]),
2368            step_state: step_timings.with_label_values(&["state"]),
2369            step_math: step_timings.with_label_values(&["math"]),
2370        }
2371    }
2372}
2373
2374/// Represents a change in a number of updates kept in a data structure
2375/// (e.g., a buffer length or capacity change).
2376#[derive(Debug)]
2377pub enum UpdateDelta {
2378    /// A negative delta in the number of updates.
2379    Negative(u64),
2380    /// A non-negative delta in the number of updates.
2381    NonNegative(u64),
2382}
2383
2384impl UpdateDelta {
2385    /// Creates a new `UpdateDelta` from the difference between a new value
2386    /// for a number of updates and the corresponding old value.
2387    pub fn new(new: usize, old: usize) -> Self {
2388        if new < old {
2389            UpdateDelta::Negative(CastFrom::cast_from(old - new))
2390        } else {
2391            UpdateDelta::NonNegative(CastFrom::cast_from(new - old))
2392        }
2393    }
2394}
2395
2396/// Metrics for the persist sink. (While this lies slightly outside the usual
2397/// abstraction boundary of the client, it's convenient to manage them together.
2398#[derive(Debug, Clone)]
2399pub struct SinkMetrics {
2400    /// Cumulative record insertions made to the correction buffer across workers
2401    correction_insertions_total: IntCounter,
2402    /// Cumulative record deletions made to the correction buffer across workers
2403    correction_deletions_total: IntCounter,
2404    /// Cumulative capacity increases made to the correction buffer across workers
2405    correction_capacity_increases_total: IntCounter,
2406    /// Cumulative capacity decreases made to the correction buffer across workers
2407    correction_capacity_decreases_total: IntCounter,
2408    /// Maximum length observed for any one correction buffer per worker
2409    correction_max_per_sink_worker_len_updates: raw::UIntGaugeVec,
2410    /// Maximum capacity observed for any one correction buffer per worker
2411    correction_max_per_sink_worker_capacity_updates: raw::UIntGaugeVec,
2412}
2413
2414impl SinkMetrics {
2415    fn new(registry: &MetricsRegistry) -> Self {
2416        SinkMetrics {
2417            correction_insertions_total: registry.register(metric!(
2418                name: "mz_persist_sink_correction_insertions_total",
2419                help: "The cumulative insertions observed on the correction buffer across workers and persist sinks.",
2420            )),
2421            correction_deletions_total: registry.register(metric!(
2422                name: "mz_persist_sink_correction_deletions_total",
2423                help: "The cumulative deletions observed on the correction buffer across workers and persist sinks.",
2424            )),
2425            correction_capacity_increases_total: registry.register(metric!(
2426                name: "mz_persist_sink_correction_capacity_increases_total",
2427                help: "The cumulative capacity increases observed on the correction buffer across workers and persist sinks.",
2428            )),
2429            correction_capacity_decreases_total: registry.register(metric!(
2430                name: "mz_persist_sink_correction_capacity_decreases_total",
2431                help: "The cumulative capacity decreases observed on the correction buffer across workers and persist sinks.",
2432            )),
2433            correction_max_per_sink_worker_len_updates: registry.register(metric!(
2434                name: "mz_persist_sink_correction_max_per_sink_worker_len_updates",
2435                help: "The maximum length observed for the correction buffer of any single persist sink per worker.",
2436                var_labels: ["worker_id"],
2437            )),
2438            correction_max_per_sink_worker_capacity_updates: registry.register(metric!(
2439                name: "mz_persist_sink_correction_max_per_sink_worker_capacity_updates",
2440                help: "The maximum capacity observed for the correction buffer of any single persist sink per worker.",
2441                var_labels: ["worker_id"],
2442            )),
2443        }
2444    }
2445
2446    /// Obtains a `SinkWorkerMetrics` instance, which allows for metric reporting
2447    /// from a specific `persist_sink` instance for a given worker. The reports will
2448    /// update metrics shared across workers, but provide per-worker contributions
2449    /// to them.
2450    pub fn for_worker(&self, worker_id: usize) -> SinkWorkerMetrics {
2451        let worker = worker_id.to_string();
2452        let correction_max_per_sink_worker_len_updates = self
2453            .correction_max_per_sink_worker_len_updates
2454            .with_label_values(&[&worker]);
2455        let correction_max_per_sink_worker_capacity_updates = self
2456            .correction_max_per_sink_worker_capacity_updates
2457            .with_label_values(&[&worker]);
2458        SinkWorkerMetrics {
2459            correction_max_per_sink_worker_len_updates,
2460            correction_max_per_sink_worker_capacity_updates,
2461        }
2462    }
2463
2464    /// Reports updates to the length and capacity of the correction buffer in the
2465    /// `write_batches` operator of a `persist_sink`.
2466    ///
2467    /// This method updates monotonic metrics based on the deltas and thus can be
2468    /// called across workers and instances of `persist_sink`.
2469    pub fn report_correction_update_deltas(
2470        &self,
2471        correction_len_delta: UpdateDelta,
2472        correction_cap_delta: UpdateDelta,
2473    ) {
2474        // Report insertions or deletions.
2475        match correction_len_delta {
2476            UpdateDelta::NonNegative(delta) => {
2477                if delta > 0 {
2478                    self.correction_insertions_total.inc_by(delta)
2479                }
2480            }
2481            UpdateDelta::Negative(delta) => self.correction_deletions_total.inc_by(delta),
2482        }
2483        // Report capacity increases or decreases.
2484        match correction_cap_delta {
2485            UpdateDelta::NonNegative(delta) => {
2486                if delta > 0 {
2487                    self.correction_capacity_increases_total.inc_by(delta)
2488                }
2489            }
2490            UpdateDelta::Negative(delta) => self.correction_capacity_decreases_total.inc_by(delta),
2491        }
2492    }
2493}
2494
2495/// Metrics for the persist sink that are labeled per-worker.
2496#[derive(Clone, Debug)]
2497pub struct SinkWorkerMetrics {
2498    correction_max_per_sink_worker_len_updates: UIntGauge,
2499    correction_max_per_sink_worker_capacity_updates: UIntGauge,
2500}
2501
2502impl SinkWorkerMetrics {
2503    /// Reports the length and capacity of the correction buffer in the `write_batches`
2504    /// operator of `persist_sink`.
2505    ///
2506    /// This method is used to update metrics that are kept per worker.
2507    pub fn report_correction_update_totals(&self, correction_len: usize, correction_cap: usize) {
2508        // Maintain per-worker peaks.
2509        let correction_len = CastFrom::cast_from(correction_len);
2510        if correction_len > self.correction_max_per_sink_worker_len_updates.get() {
2511            self.correction_max_per_sink_worker_len_updates
2512                .set(correction_len);
2513        }
2514        let correction_cap = CastFrom::cast_from(correction_cap);
2515        if correction_cap > self.correction_max_per_sink_worker_capacity_updates.get() {
2516            self.correction_max_per_sink_worker_capacity_updates
2517                .set(correction_cap);
2518        }
2519    }
2520}
2521
2522/// Metrics for the `persist_source` backpressure operator, summed over every
2523/// instance of the operator in the process. Like [SinkMetrics], these belong
2524/// to a dataflow operator rather than the client, but the client owns the only
2525/// registry the operator can reach.
2526#[derive(Debug)]
2527pub struct BackpressureMetrics {
2528    /// Bytes emitted by backpressure operators.
2529    pub emitted_bytes: IntCounter,
2530    /// Sum over live operator instances of the inflight bytes each one most
2531    /// recently stalled on. Instances contribute deltas, so the sum stays exact
2532    /// as operators start and stop.
2533    pub last_backpressured_bytes: UIntGauge,
2534    /// Bytes retired by processing downstream of backpressure operators.
2535    pub retired_bytes: IntCounter,
2536}
2537
2538impl BackpressureMetrics {
2539    fn new(registry: &MetricsRegistry) -> Self {
2540        BackpressureMetrics {
2541            emitted_bytes: registry.register(metric!(
2542                name: "mz_persist_backpressure_emitted_bytes",
2543                help: "bytes emitted by backpressure operators",
2544            )),
2545            last_backpressured_bytes: registry.register(metric!(
2546                name: "mz_persist_backpressure_last_backpressured_bytes",
2547                help: "sum over backpressure operators of the inflight bytes each last \
2548                    stalled on; not comparable to retired bytes, but nonzero growth \
2549                    indicates backpressure is happening",
2550            )),
2551            retired_bytes: registry.register(metric!(
2552                name: "mz_persist_backpressure_retired_bytes",
2553                help: "bytes retired by processing downstream of backpressure operators",
2554            )),
2555        }
2556    }
2557}
2558
2559/// A minimal set of metrics imported into honeycomb for alerting.
2560#[derive(Debug)]
2561pub struct AlertsMetrics {
2562    pub(crate) blob_failures: IntCounter,
2563    pub(crate) consensus_failures: IntCounter,
2564}
2565
2566impl AlertsMetrics {
2567    fn new(registry: &MetricsRegistry) -> Self {
2568        AlertsMetrics {
2569            blob_failures: registry.register(metric!(
2570                name: "mz_persist_blob_failures",
2571                help: "count of all blob operation failures",
2572                const_labels: {"honeycomb" => "import"},
2573            )),
2574            consensus_failures: registry.register(metric!(
2575                name: "mz_persist_consensus_failures",
2576                help: "count of determinate consensus operation failures",
2577                const_labels: {"honeycomb" => "import"},
2578            )),
2579        }
2580    }
2581}
2582
2583/// Metrics for the PubSubServer implementation.
2584#[derive(Debug)]
2585pub struct PubSubServerMetrics {
2586    pub(crate) active_connections: UIntGauge,
2587    pub(crate) broadcasted_diff_count: IntCounter,
2588    pub(crate) broadcasted_diff_bytes: IntCounter,
2589    pub(crate) broadcasted_diff_dropped_channel_full: IntCounter,
2590
2591    pub(crate) push_seconds: Counter,
2592    pub(crate) subscribe_seconds: Counter,
2593    pub(crate) unsubscribe_seconds: Counter,
2594    pub(crate) connection_cleanup_seconds: Counter,
2595
2596    pub(crate) push_call_count: IntCounter,
2597    pub(crate) subscribe_call_count: IntCounter,
2598    pub(crate) unsubscribe_call_count: IntCounter,
2599}
2600
2601impl PubSubServerMetrics {
2602    pub(crate) fn new(registry: &MetricsRegistry) -> Self {
2603        let op_timings: CounterVec = registry.register(metric!(
2604                name: "mz_persist_pubsub_server_operation_seconds",
2605                help: "time spent in pubsub server performing each operation",
2606                var_labels: ["op"],
2607        ));
2608        let call_count: IntCounterVec = registry.register(metric!(
2609                name: "mz_persist_pubsub_server_call_count",
2610                help: "count of each pubsub server message received",
2611                var_labels: ["call"],
2612        ));
2613
2614        Self {
2615            active_connections: registry.register(metric!(
2616                    name: "mz_persist_pubsub_server_active_connections",
2617                    help: "number of active connections to server",
2618            )),
2619            broadcasted_diff_count: registry.register(metric!(
2620                    name: "mz_persist_pubsub_server_broadcasted_diff_count",
2621                    help: "count of total broadcast diff messages sent",
2622            )),
2623            broadcasted_diff_bytes: registry.register(metric!(
2624                    name: "mz_persist_pubsub_server_broadcasted_diff_bytes",
2625                    help: "count of total broadcast diff bytes sent",
2626            )),
2627            broadcasted_diff_dropped_channel_full: registry.register(metric!(
2628                    name: "mz_persist_pubsub_server_broadcasted_diff_dropped_channel_full",
2629                    help: "count of diffs dropped due to full connection channel",
2630            )),
2631
2632            push_seconds: op_timings.with_label_values(&["push"]),
2633            subscribe_seconds: op_timings.with_label_values(&["subscribe"]),
2634            unsubscribe_seconds: op_timings.with_label_values(&["unsubscribe"]),
2635            connection_cleanup_seconds: op_timings.with_label_values(&["cleanup"]),
2636
2637            push_call_count: call_count.with_label_values(&["push"]),
2638            subscribe_call_count: call_count.with_label_values(&["subscribe"]),
2639            unsubscribe_call_count: call_count.with_label_values(&["unsubscribe"]),
2640        }
2641    }
2642}
2643
2644/// Metrics for the PubSubClient implementation.
2645#[derive(Debug)]
2646pub struct PubSubClientMetrics {
2647    pub sender: PubSubClientSenderMetrics,
2648    pub receiver: PubSubClientReceiverMetrics,
2649    pub grpc_connection: PubSubGrpcClientConnectionMetrics,
2650}
2651
2652impl PubSubClientMetrics {
2653    fn new(registry: &MetricsRegistry) -> Self {
2654        PubSubClientMetrics {
2655            sender: PubSubClientSenderMetrics::new(registry),
2656            receiver: PubSubClientReceiverMetrics::new(registry),
2657            grpc_connection: PubSubGrpcClientConnectionMetrics::new(registry),
2658        }
2659    }
2660}
2661
2662#[derive(Debug)]
2663pub struct PubSubGrpcClientConnectionMetrics {
2664    pub(crate) connected: UIntGauge,
2665    pub(crate) connection_established_count: IntCounter,
2666    pub(crate) connect_call_attempt_count: IntCounter,
2667    pub(crate) broadcast_recv_lagged_count: IntCounter,
2668    pub(crate) grpc_error_count: IntCounter,
2669}
2670
2671impl PubSubGrpcClientConnectionMetrics {
2672    fn new(registry: &MetricsRegistry) -> Self {
2673        Self {
2674            connected: registry.register(metric!(
2675                    name: "mz_persist_pubsub_client_grpc_connected",
2676                    help: "whether the grpc client is currently connected",
2677            )),
2678            connection_established_count: registry.register(metric!(
2679                    name: "mz_persist_pubsub_client_grpc_connection_established_count",
2680                    help: "count of grpc connection establishments to pubsub server",
2681            )),
2682            connect_call_attempt_count: registry.register(metric!(
2683                    name: "mz_persist_pubsub_client_grpc_connect_call_attempt_count",
2684                    help: "count of connection call attempts (including retries) to pubsub server",
2685            )),
2686            broadcast_recv_lagged_count: registry.register(metric!(
2687                    name: "mz_persist_pubsub_client_grpc_broadcast_recv_lagged_count",
2688                    help: "times a message was missed by broadcast receiver due to lag",
2689            )),
2690            grpc_error_count: registry.register(metric!(
2691                    name: "mz_persist_pubsub_client_grpc_error_count",
2692                    help: "count of grpc errors received",
2693            )),
2694        }
2695    }
2696}
2697
2698#[derive(Clone, Debug)]
2699pub struct PubSubClientReceiverMetrics {
2700    pub(crate) push_received: IntCounter,
2701    pub(crate) unknown_message_received: IntCounter,
2702    pub(crate) approx_diff_latency_seconds: Histogram,
2703
2704    pub(crate) state_pushed_diff_fast_path: IntCounter,
2705    pub(crate) state_pushed_diff_slow_path_succeeded: IntCounter,
2706    pub(crate) state_pushed_diff_slow_path_failed: IntCounter,
2707
2708    pub(crate) diff_applied: IntCounter,
2709    pub(crate) diff_not_applied_stale: IntCounter,
2710    pub(crate) diff_not_applied_out_of_order: IntCounter,
2711}
2712
2713impl PubSubClientReceiverMetrics {
2714    fn new(registry: &MetricsRegistry) -> Self {
2715        let call_received: IntCounterVec = registry.register(metric!(
2716                name: "mz_persist_pubsub_client_call_received",
2717                help: "times a pubsub client call was received",
2718                var_labels: ["call"],
2719        ));
2720
2721        Self {
2722            push_received: call_received.with_label_values(&["push"]),
2723            unknown_message_received: call_received.with_label_values(&["unknown"]),
2724            approx_diff_latency_seconds: registry.register(metric!(
2725                name: "mz_persist_pubsub_client_approx_diff_apply_latency_seconds",
2726                help: "histogram of (approximate) latency between sending a diff and applying it",
2727                buckets: prometheus::exponential_buckets(0.001, 2.0, 13).expect("buckets"),
2728            )),
2729
2730            state_pushed_diff_fast_path: registry.register(metric!(
2731                name: "mz_persist_pubsub_client_receiver_state_push_diff_fast_path",
2732                help: "count fast-path state push_diff calls",
2733            )),
2734            state_pushed_diff_slow_path_succeeded: registry.register(metric!(
2735                name: "mz_persist_pubsub_client_receiver_state_push_diff_slow_path_succeeded",
2736                help: "count of successful slow-path state push_diff calls",
2737            )),
2738            state_pushed_diff_slow_path_failed: registry.register(metric!(
2739                name: "mz_persist_pubsub_client_receiver_state_push_diff_slow_path_failed",
2740                help: "count of unsuccessful slow-path state push_diff calls",
2741            )),
2742            diff_applied: registry.register(metric!(
2743                name: "mz_persist_pubsub_client_receiver_diff_applied",
2744                help: "number of diffs received via pubsub that applied",
2745            )),
2746            diff_not_applied_stale: registry.register(metric!(
2747                name: "mz_persist_pubsub_client_receiver_diff_not_applied_stale",
2748                help: "number of diffs received via pubsub that did not apply due to staleness",
2749            )),
2750            diff_not_applied_out_of_order: registry.register(metric!(
2751                name: "mz_persist_pubsub_client_receiver_diff_not_applied_out_of_order",
2752                help: "number of diffs received via pubsub that did not apply due to out-of-order delivery",
2753            )),
2754        }
2755    }
2756}
2757
2758#[derive(Debug)]
2759pub struct PubSubClientSenderMetrics {
2760    pub push: PubSubClientCallMetrics,
2761    pub subscribe: PubSubClientCallMetrics,
2762    pub unsubscribe: PubSubClientCallMetrics,
2763}
2764
2765#[derive(Debug)]
2766pub struct PubSubClientCallMetrics {
2767    pub(crate) succeeded: IntCounter,
2768    pub(crate) bytes_sent: IntCounter,
2769    pub(crate) failed: IntCounter,
2770}
2771
2772impl PubSubClientSenderMetrics {
2773    fn new(registry: &MetricsRegistry) -> Self {
2774        let call_bytes_sent: IntCounterVec = registry.register(metric!(
2775                name: "mz_persist_pubsub_client_call_bytes_sent",
2776                help: "number of bytes sent for a given pubsub client call",
2777                var_labels: ["call"],
2778        ));
2779        let call_succeeded: IntCounterVec = registry.register(metric!(
2780                name: "mz_persist_pubsub_client_call_succeeded",
2781                help: "times a pubsub client call succeeded",
2782                var_labels: ["call"],
2783        ));
2784        let call_failed: IntCounterVec = registry.register(metric!(
2785                name: "mz_persist_pubsub_client_call_failed",
2786                help: "times a pubsub client call failed",
2787                var_labels: ["call"],
2788        ));
2789
2790        Self {
2791            push: PubSubClientCallMetrics {
2792                succeeded: call_succeeded.with_label_values(&["push"]),
2793                failed: call_failed.with_label_values(&["push"]),
2794                bytes_sent: call_bytes_sent.with_label_values(&["push"]),
2795            },
2796            subscribe: PubSubClientCallMetrics {
2797                succeeded: call_succeeded.with_label_values(&["subscribe"]),
2798                failed: call_failed.with_label_values(&["subscribe"]),
2799                bytes_sent: call_bytes_sent.with_label_values(&["subscribe"]),
2800            },
2801            unsubscribe: PubSubClientCallMetrics {
2802                succeeded: call_succeeded.with_label_values(&["unsubscribe"]),
2803                failed: call_failed.with_label_values(&["unsubscribe"]),
2804                bytes_sent: call_bytes_sent.with_label_values(&["unsubscribe"]),
2805            },
2806        }
2807    }
2808}
2809
2810#[derive(Debug)]
2811pub struct LocksMetrics {
2812    pub(crate) applier_read_cacheable: LockMetrics,
2813    pub(crate) applier_read_noncacheable: LockMetrics,
2814    pub(crate) applier_write: LockMetrics,
2815    pub(crate) watch: LockMetrics,
2816}
2817
2818#[derive(Debug, Clone)]
2819pub struct LockMetrics {
2820    pub(crate) acquire_count: IntCounter,
2821    pub(crate) blocking_acquire_count: IntCounter,
2822    pub(crate) blocking_seconds: Counter,
2823}
2824
2825#[derive(Debug)]
2826pub struct WatchMetrics {
2827    pub(crate) wait_woken_via_watch: IntCounter,
2828    pub(crate) wait_woken_via_sleep: IntCounter,
2829    pub(crate) wait_resolved_via_watch: IntCounter,
2830    pub(crate) wait_resolved_via_sleep: IntCounter,
2831    pub(crate) notify_sent: IntCounter,
2832    pub(crate) notify_upper_sent: IntCounter,
2833    pub(crate) notify_noop: IntCounter,
2834    pub(crate) notify_recv: IntCounter,
2835    pub(crate) notify_lagged: IntCounter,
2836    pub(crate) notify_wait_started: IntCounter,
2837    pub(crate) notify_wait_finished: IntCounter,
2838}
2839
2840impl WatchMetrics {
2841    fn new(registry: &MetricsRegistry) -> Self {
2842        WatchMetrics {
2843            wait_woken_via_watch: registry.register(metric!(
2844                name: "mz_persist_wait_woken_via_watch",
2845                help: "count of wait-for-uppers wakes via watch notify",
2846            )),
2847            wait_woken_via_sleep: registry.register(metric!(
2848                name: "mz_persist_wait_woken_via_sleep",
2849                help: "count of wait-for-uppers wakes via sleep",
2850            )),
2851            wait_resolved_via_watch: registry.register(metric!(
2852                name: "mz_persist_wait_resolved_via_watch",
2853                help: "count of wait-for-uppers resolved via watch notify",
2854            )),
2855            wait_resolved_via_sleep: registry.register(metric!(
2856                name: "mz_persist_wait_resolved_via_sleep",
2857                help: "count of wait-for-uppers resolved via sleep",
2858            )),
2859            notify_sent: registry.register(metric!(
2860                name: "mz_persist_watch_notify_sent",
2861                help: "count of watch notifications sent to a non-empty broadcast channel",
2862            )),
2863            notify_upper_sent: registry.register(metric!(
2864                name: "mz_persist_watch_notify_upper_sent",
2865                help: "count of strict shard upper advances signaled to upper waiters",
2866            )),
2867            notify_noop: registry.register(metric!(
2868                name: "mz_persist_watch_notify_noop",
2869                help: "count of watch notifications sent to an broadcast channel",
2870            )),
2871            notify_recv: registry.register(metric!(
2872                name: "mz_persist_watch_notify_recv",
2873                help: "count of watch notifications received from the broadcast channel",
2874            )),
2875            notify_lagged: registry.register(metric!(
2876                name: "mz_persist_watch_notify_lagged",
2877                help: "count of lagged events in the watch notification broadcast channel",
2878            )),
2879            notify_wait_started: registry.register(metric!(
2880                name: "mz_persist_watch_notify_wait_started",
2881                help: "count of watch wait calls started",
2882            )),
2883            notify_wait_finished: registry.register(metric!(
2884                name: "mz_persist_watch_notify_wait_finished",
2885                help: "count of watch wait calls resolved",
2886            )),
2887        }
2888    }
2889}
2890
2891#[derive(Debug)]
2892pub struct PushdownMetrics {
2893    pub(crate) parts_filtered_count: IntCounter,
2894    pub(crate) parts_filtered_bytes: IntCounter,
2895    pub(crate) parts_fetched_count: IntCounter,
2896    pub(crate) parts_fetched_bytes: IntCounter,
2897    pub(crate) parts_audited_count: IntCounter,
2898    pub(crate) parts_audited_bytes: IntCounter,
2899    pub(crate) parts_inline_count: IntCounter,
2900    pub(crate) parts_inline_bytes: IntCounter,
2901    pub(crate) parts_faked_count: IntCounter,
2902    pub(crate) parts_faked_bytes: IntCounter,
2903    pub(crate) parts_stats_trimmed_count: IntCounter,
2904    pub(crate) parts_stats_trimmed_bytes: IntCounter,
2905    pub(crate) parts_projection_trimmed_bytes: IntCounter,
2906    pub part_stats: PartStatsMetrics,
2907}
2908
2909impl PushdownMetrics {
2910    fn new(registry: &MetricsRegistry) -> Self {
2911        PushdownMetrics {
2912            parts_filtered_count: registry.register(metric!(
2913                name: "mz_persist_pushdown_parts_filtered_count",
2914                help: "count of parts filtered by pushdown",
2915            )),
2916            parts_filtered_bytes: registry.register(metric!(
2917                name: "mz_persist_pushdown_parts_filtered_bytes",
2918                help: "total size of parts filtered by pushdown in bytes",
2919            )),
2920            parts_fetched_count: registry.register(metric!(
2921                name: "mz_persist_pushdown_parts_fetched_count",
2922                help: "count of parts not filtered by pushdown",
2923            )),
2924            parts_fetched_bytes: registry.register(metric!(
2925                name: "mz_persist_pushdown_parts_fetched_bytes",
2926                help: "total size of parts not filtered by pushdown in bytes",
2927            )),
2928            parts_audited_count: registry.register(metric!(
2929                name: "mz_persist_pushdown_parts_audited_count",
2930                help: "count of parts fetched only for pushdown audit",
2931            )),
2932            parts_audited_bytes: registry.register(metric!(
2933                name: "mz_persist_pushdown_parts_audited_bytes",
2934                help: "total size of parts fetched only for pushdown audit",
2935            )),
2936            parts_inline_count: registry.register(metric!(
2937                name: "mz_persist_pushdown_parts_inline_count",
2938                help: "count of parts not fetched because they were inline",
2939            )),
2940            parts_inline_bytes: registry.register(metric!(
2941                name: "mz_persist_pushdown_parts_inline_bytes",
2942                help: "total size of parts not fetched because they were inline",
2943            )),
2944            parts_faked_count: registry.register(metric!(
2945                name: "mz_persist_pushdown_parts_faked_count",
2946                help: "count of parts faked because of aggressive projection pushdown",
2947            )),
2948            parts_faked_bytes: registry.register(metric!(
2949                name: "mz_persist_pushdown_parts_faked_bytes",
2950                help: "total size of parts replaced with fakes by aggressive projection pushdown",
2951            )),
2952            parts_stats_trimmed_count: registry.register(metric!(
2953                name: "mz_persist_pushdown_parts_stats_trimmed_count",
2954                help: "count of trimmed part stats",
2955            )),
2956            parts_stats_trimmed_bytes: registry.register(metric!(
2957                name: "mz_persist_pushdown_parts_stats_trimmed_bytes",
2958                help: "total bytes trimmed from part stats",
2959            )),
2960            parts_projection_trimmed_bytes: registry.register(metric!(
2961                name: "mz_persist_pushdown_parts_projection_trimmed_bytes",
2962                help: "total bytes trimmed from columnar data because of projection pushdown",
2963            )),
2964            part_stats: PartStatsMetrics::new(registry),
2965        }
2966    }
2967}
2968
2969#[derive(Debug)]
2970pub struct ConsolidationMetrics {
2971    pub(crate) parts_fetched: IntCounter,
2972    pub(crate) parts_skipped: IntCounter,
2973    pub(crate) parts_wasted: IntCounter,
2974    pub(crate) wrong_sort: IntCounter,
2975}
2976
2977impl ConsolidationMetrics {
2978    fn new(registry: &MetricsRegistry) -> Self {
2979        ConsolidationMetrics {
2980            parts_fetched: registry.register(metric!(
2981                name: "mz_persist_consolidation_parts_fetched_count",
2982                help: "count of parts that were fetched and used during consolidation",
2983            )),
2984            parts_skipped: registry.register(metric!(
2985                name: "mz_persist_consolidation_parts_skipped_count",
2986                help: "count of parts that were never needed during consolidation",
2987            )),
2988            parts_wasted: registry.register(metric!(
2989                name: "mz_persist_consolidation_parts_wasted_count",
2990                help: "count of parts that were fetched but not needed during consolidation",
2991            )),
2992            wrong_sort: registry.register(metric!(
2993                name: "mz_persist_consolidation_wrong_sort_count",
2994                help: "count of runs that were sorted using the wrong ordering for the current consolidation",
2995            )),
2996        }
2997    }
2998}
2999
3000#[derive(Debug)]
3001pub struct BlobMemCache {
3002    pub(crate) size_blobs: UIntGauge,
3003    pub(crate) size_bytes: UIntGauge,
3004    pub(crate) hits_blobs: IntCounter,
3005    pub(crate) hits_bytes: IntCounter,
3006    pub(crate) evictions: IntCounter,
3007}
3008
3009impl BlobMemCache {
3010    fn new(registry: &MetricsRegistry) -> Self {
3011        BlobMemCache {
3012            size_blobs: registry.register(metric!(
3013                name: "mz_persist_blob_cache_size_blobs",
3014                help: "count of blobs in the cache",
3015                const_labels: {"cache" => "mem"},
3016            )),
3017            size_bytes: registry.register(metric!(
3018                name: "mz_persist_blob_cache_size_bytes",
3019                help: "total size of blobs in the cache",
3020                const_labels: {"cache" => "mem"},
3021            )),
3022            hits_blobs: registry.register(metric!(
3023                name: "mz_persist_blob_cache_hits_blobs",
3024                help: "count of blobs served via cache instead of s3",
3025                const_labels: {"cache" => "mem"},
3026            )),
3027            hits_bytes: registry.register(metric!(
3028                name: "mz_persist_blob_cache_hits_bytes",
3029                help: "total size of blobs served via cache instead of s3",
3030                const_labels: {"cache" => "mem"},
3031            )),
3032            evictions: registry.register(metric!(
3033                name: "mz_persist_blob_cache_evictions",
3034                help: "count of capacity-based cache evictions",
3035                const_labels: {"cache" => "mem"},
3036            )),
3037        }
3038    }
3039}
3040
3041#[derive(Debug)]
3042pub struct SemaphoreMetrics {
3043    cfg: PersistConfig,
3044    registry: MetricsRegistry,
3045    fetch: OnceCell<MetricsSemaphore>,
3046}
3047
3048impl SemaphoreMetrics {
3049    fn new(cfg: PersistConfig, registry: MetricsRegistry) -> Self {
3050        SemaphoreMetrics {
3051            cfg,
3052            registry,
3053            fetch: OnceCell::new(),
3054        }
3055    }
3056
3057    /// We can't easily change the number of permits, and the dyncfgs are all
3058    /// set to defaults on process start, so make sure we only initialize the
3059    /// semaphore once we've synced dyncfgs at least once.
3060    async fn fetch(&self) -> &MetricsSemaphore {
3061        if let Some(x) = self.fetch.get() {
3062            // Common case of already initialized avoids the cloning below.
3063            return x;
3064        }
3065        let cfg = self.cfg.clone();
3066        let registry = self.registry.clone();
3067        let init = async move {
3068            let total_permits = match cfg.announce_memory_limit {
3069                // Non-cc replicas have the old physical flow control mechanism,
3070                // so only apply this one on cc replicas.
3071                Some(mem) if cfg.is_cc_active => {
3072                    // We can't easily adjust the number of permits later, so
3073                    // make sure we've synced dyncfg values at least once.
3074                    info!("fetch semaphore awaiting first dyncfg values");
3075                    let () = cfg.configs_synced_once().await;
3076                    let total_permits = usize::cast_lossy(
3077                        f64::cast_lossy(mem) * FETCH_SEMAPHORE_PERMIT_ADJUSTMENT.get(&cfg),
3078                    );
3079                    info!("fetch_semaphore got first dyncfg values");
3080                    total_permits
3081                }
3082                Some(_) | None => Semaphore::MAX_PERMITS,
3083            };
3084            MetricsSemaphore::new(&registry, "fetch", total_permits)
3085        };
3086        self.fetch.get_or_init(|| init).await
3087    }
3088
3089    pub(crate) async fn acquire_fetch_permits(&self, encoded_size_bytes: usize) -> MetricsPermits {
3090        // Adjust the requested permits to account for the difference between
3091        // encoded_size_bytes and the decoded size in lgalloc.
3092        let requested_permits = f64::cast_lossy(encoded_size_bytes);
3093        let requested_permits = requested_permits * FETCH_SEMAPHORE_COST_ADJUSTMENT.get(&self.cfg);
3094        let requested_permits = usize::cast_lossy(requested_permits);
3095        self.fetch().await.acquire_permits(requested_permits).await
3096    }
3097}
3098
3099#[derive(Debug)]
3100pub struct MetricsSemaphore {
3101    name: &'static str,
3102    semaphore: Arc<Semaphore>,
3103    total_permits: usize,
3104    acquire_count: IntCounter,
3105    blocking_count: IntCounter,
3106    blocking_seconds: Counter,
3107    acquired_permits: IntCounter,
3108    released_permits: IntCounter,
3109    _available_permits: ComputedUIntGauge,
3110}
3111
3112impl MetricsSemaphore {
3113    pub fn new(registry: &MetricsRegistry, name: &'static str, total_permits: usize) -> Self {
3114        let total_permits = std::cmp::min(total_permits, Semaphore::MAX_PERMITS);
3115        // TODO: Sadly, tokio::sync::Semaphore makes it difficult to have a
3116        // dynamic total_permits count.
3117        let semaphore = Arc::new(Semaphore::new(total_permits));
3118        MetricsSemaphore {
3119            name,
3120            total_permits,
3121            acquire_count: registry.register(metric!(
3122                name: "mz_persist_semaphore_acquire_count",
3123                help: "count of acquire calls (not acquired permits count)",
3124                const_labels: {"name" => name},
3125            )),
3126            blocking_count: registry.register(metric!(
3127                name: "mz_persist_semaphore_blocking_count",
3128                help: "count of acquire calls that had to block",
3129                const_labels: {"name" => name},
3130            )),
3131            blocking_seconds: registry.register(metric!(
3132                name: "mz_persist_semaphore_blocking_seconds",
3133                help: "total time spent blocking on permit acquisition",
3134                const_labels: {"name" => name},
3135            )),
3136            acquired_permits: registry.register(metric!(
3137                name: "mz_persist_semaphore_acquired_permits",
3138                help: "total sum of acquired permits",
3139                const_labels: {"name" => name},
3140            )),
3141            released_permits: registry.register(metric!(
3142                name: "mz_persist_semaphore_released_permits",
3143                help: "total sum of released permits",
3144                const_labels: {"name" => name},
3145            )),
3146            _available_permits: registry.register_computed_gauge(
3147                metric!(
3148                    name: "mz_persist_semaphore_available_permits",
3149                    help: "currently available permits according to the semaphore",
3150                ),
3151                {
3152                    let semaphore = Arc::clone(&semaphore);
3153                    move || u64::cast_from(semaphore.available_permits())
3154                },
3155            ),
3156            semaphore,
3157        }
3158    }
3159
3160    pub async fn acquire_permits(&self, requested_permits: usize) -> MetricsPermits {
3161        // HACK: Cap the request at the total permit count. This prevents
3162        // deadlock, even if the cfg gets set to some small value.
3163        let total_permits = u32::try_from(self.total_permits).unwrap_or(u32::MAX);
3164        let requested_permits = u32::try_from(requested_permits).unwrap_or(u32::MAX);
3165        let requested_permits = std::cmp::min(requested_permits, total_permits);
3166        let wrap = |_permit| {
3167            self.acquired_permits.inc_by(u64::from(requested_permits));
3168            MetricsPermits {
3169                _permit,
3170                released_metric: self.released_permits.clone(),
3171                count: requested_permits,
3172            }
3173        };
3174
3175        // Special-case non-blocking happy path.
3176        self.acquire_count.inc();
3177        match Arc::clone(&self.semaphore).try_acquire_many_owned(requested_permits) {
3178            Ok(x) => return wrap(x),
3179            Err(_) => {}
3180        };
3181
3182        // Sad path, gotta block.
3183        self.blocking_count.inc();
3184        let start = Instant::now();
3185        let ret = Arc::clone(&self.semaphore)
3186            .acquire_many_owned(requested_permits)
3187            .instrument(info_span!("acquire_permits"))
3188            .await;
3189        let elapsed = start.elapsed();
3190        self.blocking_seconds.inc_by(elapsed.as_secs_f64());
3191        debug!(
3192            "acquisition of {} {} permits blocked for {:?}",
3193            self.name, requested_permits, elapsed
3194        );
3195        wrap(ret.expect("semaphore is never closed"))
3196    }
3197}
3198
3199#[derive(Debug)]
3200pub struct MetricsPermits {
3201    _permit: OwnedSemaphorePermit,
3202    released_metric: IntCounter,
3203    count: u32,
3204}
3205
3206impl Drop for MetricsPermits {
3207    fn drop(&mut self) {
3208        self.released_metric.inc_by(u64::from(self.count))
3209    }
3210}
3211
3212#[derive(Debug)]
3213pub struct ExternalOpMetrics {
3214    started: IntCounter,
3215    succeeded: IntCounter,
3216    failed: IntCounter,
3217    bytes: IntCounter,
3218    seconds: Counter,
3219    seconds_histogram: Option<Histogram>,
3220    alerts_metrics: Arc<AlertsMetrics>,
3221}
3222
3223impl ExternalOpMetrics {
3224    async fn run_op<R, F, OpFn, ErrFn>(
3225        &self,
3226        op_fn: OpFn,
3227        on_err_fn: ErrFn,
3228    ) -> Result<R, ExternalError>
3229    where
3230        F: std::future::Future<Output = Result<R, ExternalError>>,
3231        OpFn: FnOnce() -> F,
3232        ErrFn: FnOnce(&AlertsMetrics, &ExternalError),
3233    {
3234        self.started.inc();
3235        let start = Instant::now();
3236        let res = op_fn().await;
3237        let elapsed_seconds = start.elapsed().as_secs_f64();
3238        self.seconds.inc_by(elapsed_seconds);
3239        if let Some(h) = &self.seconds_histogram {
3240            h.observe(elapsed_seconds);
3241        }
3242        match res.as_ref() {
3243            Ok(_) => self.succeeded.inc(),
3244            Err(err) => {
3245                self.failed.inc();
3246                on_err_fn(&self.alerts_metrics, err);
3247            }
3248        };
3249        res
3250    }
3251
3252    fn run_stream<'a, R: 'a, S, OpFn, ErrFn>(
3253        &'a self,
3254        op_fn: OpFn,
3255        mut on_err_fn: ErrFn,
3256    ) -> impl futures::Stream<Item = Result<R, ExternalError>> + 'a
3257    where
3258        S: futures::Stream<Item = Result<R, ExternalError>> + Unpin + 'a,
3259        OpFn: FnOnce() -> S,
3260        ErrFn: FnMut(&AlertsMetrics, &ExternalError) + 'a,
3261    {
3262        self.started.inc();
3263        let start = Instant::now();
3264        let mut stream = op_fn();
3265        stream! {
3266            let mut succeeded = true;
3267            while let Some(res) = stream.next().await {
3268                if let Err(err) = res.as_ref() {
3269                    on_err_fn(&self.alerts_metrics, err);
3270                    succeeded = false;
3271                }
3272                yield res;
3273            }
3274            if succeeded {
3275                self.succeeded.inc()
3276            } else {
3277                self.failed.inc()
3278            }
3279            let elapsed_seconds = start.elapsed().as_secs_f64();
3280            self.seconds.inc_by(elapsed_seconds);
3281            if let Some(h) = &self.seconds_histogram {
3282                h.observe(elapsed_seconds);
3283            }
3284        }
3285    }
3286}
3287
3288#[derive(Debug)]
3289pub struct BlobMetrics {
3290    set: ExternalOpMetrics,
3291    get: ExternalOpMetrics,
3292    list_keys: ExternalOpMetrics,
3293    delete: ExternalOpMetrics,
3294    restore: ExternalOpMetrics,
3295    delete_noop: IntCounter,
3296    blob_sizes: Histogram,
3297    pub rtt_latency: Gauge,
3298}
3299
3300#[derive(Debug)]
3301pub struct MetricsBlob {
3302    blob: Arc<dyn Blob>,
3303    metrics: Arc<Metrics>,
3304}
3305
3306impl MetricsBlob {
3307    pub fn new(blob: Arc<dyn Blob>, metrics: Arc<Metrics>) -> Self {
3308        MetricsBlob { blob, metrics }
3309    }
3310
3311    fn on_err(alerts_metrics: &AlertsMetrics, _err: &ExternalError) {
3312        alerts_metrics.blob_failures.inc()
3313    }
3314}
3315
3316#[async_trait]
3317impl Blob for MetricsBlob {
3318    #[instrument(name = "blob::get", fields(shard=blob_key_shard_id(key)))]
3319    async fn get(&self, key: &str) -> Result<Option<SegmentedBytes>, ExternalError> {
3320        let res = self
3321            .metrics
3322            .blob
3323            .get
3324            .run_op(|| self.blob.get(key), Self::on_err)
3325            .await;
3326        if let Ok(Some(value)) = res.as_ref() {
3327            self.metrics
3328                .blob
3329                .get
3330                .bytes
3331                .inc_by(u64::cast_from(value.len()));
3332        }
3333        res
3334    }
3335
3336    #[instrument(name = "blob::list_keys_and_metadata", fields(shard=blob_key_shard_id(key_prefix)))]
3337    async fn list_keys_and_metadata(
3338        &self,
3339        key_prefix: &str,
3340        f: &mut (dyn FnMut(BlobMetadata) + Send + Sync),
3341    ) -> Result<(), ExternalError> {
3342        let mut byte_total = 0;
3343        let mut instrumented = |blob_metadata: BlobMetadata| {
3344            // Track the size of the _keys_, not the blobs, so that we get a
3345            // sense for how much network bandwidth these calls are using.
3346            byte_total += blob_metadata.key.len();
3347            f(blob_metadata)
3348        };
3349
3350        let res = self
3351            .metrics
3352            .blob
3353            .list_keys
3354            .run_op(
3355                || {
3356                    self.blob
3357                        .list_keys_and_metadata(key_prefix, &mut instrumented)
3358                },
3359                Self::on_err,
3360            )
3361            .await;
3362
3363        self.metrics
3364            .blob
3365            .list_keys
3366            .bytes
3367            .inc_by(u64::cast_from(byte_total));
3368
3369        res
3370    }
3371
3372    #[instrument(name = "blob::set", fields(shard=blob_key_shard_id(key),size_bytes=value.len()))]
3373    async fn set(&self, key: &str, value: Bytes) -> Result<(), ExternalError> {
3374        let bytes = value.len();
3375        let res = self
3376            .metrics
3377            .blob
3378            .set
3379            .run_op(|| self.blob.set(key, value), Self::on_err)
3380            .await;
3381        if res.is_ok() {
3382            self.metrics.blob.set.bytes.inc_by(u64::cast_from(bytes));
3383            self.metrics.blob.blob_sizes.observe(f64::cast_lossy(bytes));
3384        }
3385        res
3386    }
3387
3388    #[instrument(name = "blob::delete", fields(shard=blob_key_shard_id(key)))]
3389    async fn delete(&self, key: &str) -> Result<Option<usize>, ExternalError> {
3390        let bytes = self
3391            .metrics
3392            .blob
3393            .delete
3394            .run_op(|| self.blob.delete(key), Self::on_err)
3395            .await?;
3396        if let Some(bytes) = bytes {
3397            self.metrics.blob.delete.bytes.inc_by(u64::cast_from(bytes));
3398        } else {
3399            self.metrics.blob.delete_noop.inc();
3400        }
3401        Ok(bytes)
3402    }
3403
3404    async fn restore(&self, key: &str) -> Result<(), ExternalError> {
3405        self.metrics
3406            .blob
3407            .restore
3408            .run_op(|| self.blob.restore(key), Self::on_err)
3409            .await
3410    }
3411}
3412
3413#[derive(Debug)]
3414pub struct ConsensusMetrics {
3415    list_keys: ExternalOpMetrics,
3416    head: ExternalOpMetrics,
3417    compare_and_set: ExternalOpMetrics,
3418    scan: ExternalOpMetrics,
3419    truncate: ExternalOpMetrics,
3420    truncated_count: IntCounter,
3421    pub rtt_latency: Gauge,
3422}
3423
3424#[derive(Debug)]
3425pub struct MetricsConsensus {
3426    consensus: Arc<dyn Consensus>,
3427    metrics: Arc<Metrics>,
3428}
3429
3430impl MetricsConsensus {
3431    pub fn new(consensus: Arc<dyn Consensus>, metrics: Arc<Metrics>) -> Self {
3432        MetricsConsensus { consensus, metrics }
3433    }
3434
3435    fn on_err(alerts_metrics: &AlertsMetrics, err: &ExternalError) {
3436        // As of 2022-09-06, regular determinate errors are expected in
3437        // Consensus (i.e. "txn conflict, please retry"), so only count the
3438        // indeterminate ones.
3439        if let ExternalError::Indeterminate(_) = err {
3440            alerts_metrics.consensus_failures.inc()
3441        }
3442    }
3443}
3444
3445#[async_trait]
3446impl Consensus for MetricsConsensus {
3447    fn list_keys(&self) -> ResultStream<'_, String> {
3448        Box::pin(
3449            self.metrics
3450                .consensus
3451                .list_keys
3452                .run_stream(|| self.consensus.list_keys(), Self::on_err),
3453        )
3454    }
3455
3456    #[instrument(name = "consensus::head", fields(shard=key))]
3457    async fn head(&self, key: &str) -> Result<Option<VersionedData>, ExternalError> {
3458        let res = self
3459            .metrics
3460            .consensus
3461            .head
3462            .run_op(|| self.consensus.head(key), Self::on_err)
3463            .await;
3464        if let Ok(Some(data)) = res.as_ref() {
3465            self.metrics
3466                .consensus
3467                .head
3468                .bytes
3469                .inc_by(u64::cast_from(data.data.len()));
3470        }
3471        res
3472    }
3473
3474    #[instrument(name = "consensus::compare_and_set", fields(shard=key,size_bytes=new.data.len()))]
3475    async fn compare_and_set(
3476        &self,
3477        key: &str,
3478        new: VersionedData,
3479    ) -> Result<CaSResult, ExternalError> {
3480        let bytes = new.data.len();
3481        let res = self
3482            .metrics
3483            .consensus
3484            .compare_and_set
3485            .run_op(|| self.consensus.compare_and_set(key, new), Self::on_err)
3486            .await;
3487        match res.as_ref() {
3488            Ok(CaSResult::Committed) => self
3489                .metrics
3490                .consensus
3491                .compare_and_set
3492                .bytes
3493                .inc_by(u64::cast_from(bytes)),
3494            Ok(CaSResult::ExpectationMismatch) | Err(_) => {}
3495        }
3496        res
3497    }
3498
3499    #[instrument(name = "consensus::scan", fields(shard=key))]
3500    async fn scan(
3501        &self,
3502        key: &str,
3503        from: SeqNo,
3504        limit: usize,
3505    ) -> Result<Vec<VersionedData>, ExternalError> {
3506        let res = self
3507            .metrics
3508            .consensus
3509            .scan
3510            .run_op(|| self.consensus.scan(key, from, limit), Self::on_err)
3511            .await;
3512        if let Ok(dataz) = res.as_ref() {
3513            let bytes: usize = dataz.iter().map(|x| x.data.len()).sum();
3514            self.metrics
3515                .consensus
3516                .scan
3517                .bytes
3518                .inc_by(u64::cast_from(bytes));
3519        }
3520        res
3521    }
3522
3523    #[instrument(name = "consensus::truncate", fields(shard=key))]
3524    async fn truncate(&self, key: &str, seqno: SeqNo) -> Result<Option<usize>, ExternalError> {
3525        let metrics = &self.metrics.consensus;
3526        let deleted = metrics
3527            .truncate
3528            .run_op(|| self.consensus.truncate(key, seqno), Self::on_err)
3529            .await?;
3530        if let Some(deleted) = deleted {
3531            metrics.truncated_count.inc_by(u64::cast_from(deleted));
3532        }
3533        Ok(deleted)
3534    }
3535}
3536
3537/// A standard set of metrics for an async task. Call [TaskMetrics::instrument_task] to instrument
3538/// a future and report its metrics for this task type.
3539#[derive(Debug, Clone)]
3540pub struct TaskMetrics {
3541    f64_gauges: Vec<(Gauge, fn(&tokio_metrics::TaskMetrics) -> f64)>,
3542    u64_gauges: Vec<(
3543        GenericGauge<AtomicU64>,
3544        fn(&tokio_metrics::TaskMetrics) -> u64,
3545    )>,
3546    monitor: TaskMonitor,
3547}
3548
3549impl TaskMetrics {
3550    pub fn new(name: &str) -> Self {
3551        let monitor = TaskMonitor::new();
3552        Self {
3553            f64_gauges: vec![
3554                (
3555                    Gauge::make_collector(metric!(
3556                        name: "mz_persist_task_total_idle_duration",
3557                        help: "Seconds of time spent idling, ie. waiting for a task to be woken up.",
3558                        const_labels: {"name" => name}
3559                    )),
3560                    |m| m.total_idle_duration.as_secs_f64(),
3561                ),
3562                (
3563                    Gauge::make_collector(metric!(
3564                        name: "mz_persist_task_total_scheduled_duration",
3565                        help: "Seconds of time spent scheduled, ie. ready to poll but not yet polled.",
3566                        const_labels: {"name" => name}
3567                    )),
3568                    |m| m.total_scheduled_duration.as_secs_f64(),
3569                ),
3570            ],
3571            u64_gauges: vec![
3572                (
3573                    MakeCollector::make_collector(metric!(
3574                        name: "mz_persist_task_total_scheduled_count",
3575                        help: "The total number of task schedules. Useful for computing the average scheduled time.",
3576                        const_labels: {"name" => name}
3577                    )),
3578                    |m| m.total_scheduled_count,
3579                ),
3580                (
3581                    MakeCollector::make_collector(metric!(
3582                        name: "mz_persist_task_total_idled_count",
3583                        help: "The total number of task idles. Useful for computing the average idle time.",
3584                        const_labels: {"name" => name}
3585                    ,
3586                    )),
3587                    |m| m.total_idled_count,
3588                ),
3589            ],
3590            monitor,
3591        }
3592    }
3593
3594    /// Instrument the provided future. The expectation is that the result will be executed
3595    /// as a task. (See [TaskMonitor::instrument] for more context.)
3596    pub fn instrument_task<F>(&self, task: F) -> tokio_metrics::Instrumented<F> {
3597        TaskMonitor::instrument(&self.monitor, task)
3598    }
3599}
3600
3601impl Collector for TaskMetrics {
3602    fn desc(&self) -> Vec<&Desc> {
3603        let mut descs = Vec::with_capacity(self.f64_gauges.len() + self.u64_gauges.len());
3604        for (g, _) in &self.f64_gauges {
3605            descs.extend(g.desc());
3606        }
3607        for (g, _) in &self.u64_gauges {
3608            descs.extend(g.desc());
3609        }
3610        descs
3611    }
3612
3613    fn collect(&self) -> Vec<MetricFamily> {
3614        let mut families = Vec::with_capacity(self.f64_gauges.len() + self.u64_gauges.len());
3615        let metrics = self.monitor.cumulative();
3616        for (g, metrics_fn) in &self.f64_gauges {
3617            g.set(metrics_fn(&metrics));
3618            families.extend(g.collect());
3619        }
3620        for (g, metrics_fn) in &self.u64_gauges {
3621            g.set(metrics_fn(&metrics));
3622            families.extend(g.collect());
3623        }
3624        families
3625    }
3626}
3627
3628#[derive(Debug)]
3629pub struct TasksMetrics {
3630    pub heartbeat_read: TaskMetrics,
3631}
3632
3633impl TasksMetrics {
3634    fn new(registry: &MetricsRegistry) -> Self {
3635        let heartbeat_read = TaskMetrics::new("heartbeat_read");
3636        registry.register_collector(heartbeat_read.clone());
3637        TasksMetrics { heartbeat_read }
3638    }
3639}
3640
3641#[derive(Debug)]
3642pub struct SchemaMetrics {
3643    pub(crate) cache_fetch_state_count: IntCounter,
3644    pub(crate) cache_schema: SchemaCacheMetrics,
3645    pub(crate) cache_migration: SchemaCacheMetrics,
3646    pub(crate) migration_count_same: IntCounter,
3647    pub(crate) migration_count_codec: IntCounter,
3648    pub(crate) migration_count_either: IntCounter,
3649    pub(crate) migration_len_legacy_codec: IntCounter,
3650    pub(crate) migration_len_either_codec: IntCounter,
3651    pub(crate) migration_len_either_arrow: IntCounter,
3652    pub(crate) migration_new_count: IntCounter,
3653    pub(crate) migration_new_seconds: Counter,
3654    pub(crate) migration_migrate_seconds: Counter,
3655}
3656
3657impl SchemaMetrics {
3658    fn new(registry: &MetricsRegistry) -> Self {
3659        let cached: IntCounterVec = registry.register(metric!(
3660            name: "mz_persist_schema_cache_cached_count",
3661            help: "count of schema cache entries served from cache",
3662            var_labels: ["op"],
3663        ));
3664        let computed: IntCounterVec = registry.register(metric!(
3665            name: "mz_persist_schema_cache_computed_count",
3666            help: "count of schema cache entries computed",
3667            var_labels: ["op"],
3668        ));
3669        let unavailable: IntCounterVec = registry.register(metric!(
3670            name: "mz_persist_schema_cache_unavailable_count",
3671            help: "count of schema cache entries unavailable at current state",
3672            var_labels: ["op"],
3673        ));
3674        let added: IntCounterVec = registry.register(metric!(
3675            name: "mz_persist_schema_cache_added_count",
3676            help: "count of schema cache entries added",
3677            var_labels: ["op"],
3678        ));
3679        let dropped: IntCounterVec = registry.register(metric!(
3680            name: "mz_persist_schema_cache_dropped_count",
3681            help: "count of schema cache entries dropped",
3682            var_labels: ["op"],
3683        ));
3684        let cache = |name| SchemaCacheMetrics {
3685            cached_count: cached.with_label_values(&[name]),
3686            computed_count: computed.with_label_values(&[name]),
3687            unavailable_count: unavailable.with_label_values(&[name]),
3688            added_count: added.with_label_values(&[name]),
3689            dropped_count: dropped.with_label_values(&[name]),
3690        };
3691        let migration_count: IntCounterVec = registry.register(metric!(
3692            name: "mz_persist_schema_migration_count",
3693            help: "count of fetch part migrations",
3694            var_labels: ["op"],
3695        ));
3696        let migration_len: IntCounterVec = registry.register(metric!(
3697            name: "mz_persist_schema_migration_len",
3698            help: "count of migrated update records",
3699            var_labels: ["op"],
3700        ));
3701        SchemaMetrics {
3702            cache_fetch_state_count: registry.register(metric!(
3703                name: "mz_persist_schema_cache_fetch_state_count",
3704                help: "count of state fetches by the schema cache",
3705            )),
3706            cache_schema: cache("schema"),
3707            cache_migration: cache("migration"),
3708            migration_count_same: migration_count.with_label_values(&["same"]),
3709            migration_count_codec: migration_count.with_label_values(&["codec"]),
3710            migration_count_either: migration_count.with_label_values(&["either"]),
3711            migration_len_legacy_codec: migration_len.with_label_values(&["legacy_codec"]),
3712            migration_len_either_codec: migration_len.with_label_values(&["either_codec"]),
3713            migration_len_either_arrow: migration_len.with_label_values(&["either_arrow"]),
3714            migration_new_count: registry.register(metric!(
3715                name: "mz_persist_schema_migration_new_count",
3716                help: "count of migrations constructed",
3717            )),
3718            migration_new_seconds: registry.register(metric!(
3719                name: "mz_persist_schema_migration_new_seconds",
3720                help: "seconds spent constructing migration logic",
3721            )),
3722            migration_migrate_seconds: registry.register(metric!(
3723                name: "mz_persist_schema_migration_migrate_seconds",
3724                help: "seconds spent applying migration logic",
3725            )),
3726        }
3727    }
3728}
3729
3730#[derive(Debug, Clone)]
3731pub struct SchemaCacheMetrics {
3732    pub(crate) cached_count: IntCounter,
3733    pub(crate) computed_count: IntCounter,
3734    pub(crate) unavailable_count: IntCounter,
3735    pub(crate) added_count: IntCounter,
3736    pub(crate) dropped_count: IntCounter,
3737}
3738
3739#[derive(Debug)]
3740pub struct InlineMetrics {
3741    pub(crate) part_commit_count: IntCounter,
3742    pub(crate) part_commit_bytes: IntCounter,
3743    pub(crate) backpressure: BatchWriteMetrics,
3744}
3745
3746impl InlineMetrics {
3747    fn new(registry: &MetricsRegistry) -> Self {
3748        InlineMetrics {
3749            part_commit_count: registry.register(metric!(
3750                name: "mz_persist_inline_part_commit_count",
3751                help: "count of inline parts committed to state",
3752            )),
3753            part_commit_bytes: registry.register(metric!(
3754                name: "mz_persist_inline_part_commit_bytes",
3755                help: "total size of of inline parts committed to state",
3756            )),
3757            backpressure: BatchWriteMetrics::new(registry, "inline_backpressure"),
3758        }
3759    }
3760}
3761
3762fn blob_key_shard_id(key: &str) -> Option<String> {
3763    let (shard_id, _) = BlobKey::parse_ids(key).ok()?;
3764    Some(shard_id.to_string())
3765}
3766
3767/// Encode a frontier into an i64 acceptable for use in metrics.
3768pub fn encode_ts_metric<T: Codec64>(ts: &Antichain<T>) -> i64 {
3769    // We have two problems in mapping a persist frontier into a metric.
3770    // First is that we only have a `T: Timestamp+Codec64`. Second, is
3771    // mapping an antichain to a single counter value. We solve both by
3772    // taking advantage of the fact that in practice, timestamps in mz are
3773    // currently always a u64 (and if we switch them, it will be to an i64).
3774    // This means that for all values that mz would actually produce,
3775    // interpreting the encoded bytes as a little-endian i64 will work.
3776    // Both of them impl PartialOrder, so in practice, there will always be
3777    // zero or one elements in the antichain.
3778    match ts.elements().first() {
3779        Some(ts) => i64::from_le_bytes(Codec64::encode(ts)),
3780        None => i64::MAX,
3781    }
3782}
3783
3784#[cfg(test)]
3785mod tests {
3786    use std::collections::BTreeSet;
3787
3788    use mz_dyncfg::ConfigUpdates;
3789    use mz_ore::metrics::aggregation::PERCENTILES;
3790
3791    use crate::cfg::SHARD_METRICS;
3792
3793    use super::*;
3794
3795    #[mz_ore::test]
3796    fn shards_aggregate_metrics_one_pass() {
3797        let registry = MetricsRegistry::new();
3798        let configs = Arc::clone(&PersistConfig::new_for_tests().configs);
3799        let shards = ShardsMetrics::new(&registry, Arc::clone(&configs));
3800        let agg = ShardsAggregateMetrics::new(Arc::clone(&shards.shards), configs);
3801        let a = shards.shard(&ShardId::new(), "a");
3802        let b = shards.shard(&ShardId::new(), "b");
3803
3804        // collect() recomputes both gauges from a single walk of the map.
3805        agg.collect();
3806        assert_eq!(agg.count.get(), 2);
3807        assert_eq!(agg.stale_count.get(), 0);
3808
3809        a.stale.store(true, Ordering::Relaxed);
3810        agg.collect();
3811        assert_eq!(agg.count.get(), 2);
3812        assert_eq!(agg.stale_count.get(), 1);
3813
3814        b.stale.store(true, Ordering::Relaxed);
3815        agg.collect();
3816        assert_eq!(agg.stale_count.get(), 2);
3817
3818        // Flipping back and dropping a handle both lower the counts: the gauge
3819        // is recomputed, not ticked.
3820        a.stale.store(false, Ordering::Relaxed);
3821        drop(b);
3822        agg.collect();
3823        assert_eq!(agg.count.get(), 1);
3824        assert_eq!(agg.stale_count.get(), 0);
3825    }
3826
3827    #[mz_ore::test]
3828    fn per_shard_families_match_registrations() {
3829        let cfg = PersistConfig::new_for_tests();
3830        set_shard_metrics(&cfg, ShardMetricsExport::Both);
3831        let registry = MetricsRegistry::new();
3832        let metrics = Metrics::new(&cfg, &registry);
3833        let shard = metrics.shards.shard(&ShardId::new(), "s");
3834        shard.set_batch_part_versions(["v1"].into_iter());
3835        let gathered = registry.gather();
3836
3837        let registered: BTreeSet<_> = gathered
3838            .iter()
3839            .filter(|f| !f.name().ends_with("_topk"))
3840            .filter(|f| {
3841                f.get_metric()
3842                    .iter()
3843                    .any(|m| m.get_label().iter().any(|l| l.name() == "shard"))
3844            })
3845            .map(|f| f.name())
3846            .collect();
3847        let listed: BTreeSet<_> = PER_SHARD_FAMILIES.iter().map(|f| f.name).collect();
3848        assert_eq!(registered, listed);
3849
3850        // `ShardGauge::help` is a second copy of the registered help, and
3851        // becomes the `_percentile` / `_topk` help. Keep the wording the same.
3852        for (name, gauge) in shard_gauges() {
3853            let registered = find(&gathered, name)
3854                .unwrap_or_else(|| panic!("{name} not gathered"))
3855                .help();
3856            assert_eq!(gauge.help, registered, "{name}");
3857        }
3858
3859        let gathered: BTreeSet<_> = gathered.iter().map(|f| f.name()).collect();
3860        for family in PER_SHARD_FAMILIES {
3861            if let ShardAggregate::Counter { total } = family.aggregate {
3862                assert!(gathered.contains(total), "{total} not gathered");
3863            }
3864        }
3865    }
3866
3867    #[mz_ore::test]
3868    fn every_shard_gauge_has_its_aggregates() {
3869        let registry = MetricsRegistry::new();
3870        let configs = Arc::clone(&PersistConfig::new_for_tests().configs);
3871        let shards = ShardsMetrics::new(&registry, Arc::clone(&configs));
3872        let agg = ShardsAggregateMetrics::new(Arc::clone(&shards.shards), configs);
3873        let declared: BTreeSet<_> = agg.desc().iter().map(|d| d.fq_name.clone()).collect();
3874        for (gauge, g) in shard_gauges() {
3875            for suffix in ["_percentile", "_topk"] {
3876                let name = format!("{gauge}{suffix}");
3877                assert!(declared.contains(&name), "{name} not declared");
3878            }
3879            let sum = format!("{gauge}_sum");
3880            assert_eq!(declared.contains(&sum), g.sum, "{sum}");
3881        }
3882    }
3883
3884    fn find<'a>(families: &'a [MetricFamily], name: &str) -> Option<&'a MetricFamily> {
3885        families.iter().find(|f| f.name() == name)
3886    }
3887
3888    fn label<'a>(m: &'a prometheus::proto::Metric, name: &str) -> &'a str {
3889        m.get_label()
3890            .iter()
3891            .find(|l| l.name() == name)
3892            .map(|l| l.value())
3893            .unwrap_or_else(|| panic!("no label {name}"))
3894    }
3895
3896    /// The `(label, value)` pairs of a gauge family, as a set since gather
3897    /// order is not insertion order.
3898    fn gauge_set(
3899        families: &[MetricFamily],
3900        name: &str,
3901        label_name: &str,
3902    ) -> BTreeSet<(String, u64)> {
3903        find(families, name).map_or_else(BTreeSet::new, |f| {
3904            f.get_metric()
3905                .iter()
3906                .map(|m| {
3907                    (
3908                        label(m, label_name).to_string(),
3909                        u64::cast_lossy(m.get_gauge().value()),
3910                    )
3911                })
3912                .collect()
3913        })
3914    }
3915
3916    fn scalar(families: &[MetricFamily], name: &str) -> f64 {
3917        let f = find(families, name).unwrap_or_else(|| panic!("{name} not gathered"));
3918        let m = &f.get_metric()[0];
3919        if f.get_field_type() == prometheus::proto::MetricType::COUNTER {
3920            m.get_counter().value()
3921        } else {
3922            m.get_gauge().value()
3923        }
3924    }
3925
3926    /// Builds `Metrics` on a fresh registry with `n` shards that populate
3927    /// every per-shard family.
3928    fn metrics_with_shards(n: u64) -> (PersistConfig, MetricsRegistry, Vec<Arc<ShardMetrics>>) {
3929        let cfg = PersistConfig::new_for_tests();
3930        let registry = MetricsRegistry::new();
3931        let metrics = Metrics::new(&cfg, &registry);
3932        let shards = (1..=n)
3933            .map(|i| {
3934                let shard = metrics.shards.shard(&ShardId::new(), &format!("s{i}"));
3935                shard.usage_current_state_batches_bytes.set(i);
3936                shard.set_batch_part_versions(["v1"].into_iter());
3937                shard
3938            })
3939            .collect();
3940        (cfg, registry, shards)
3941    }
3942
3943    fn set_shard_metrics(cfg: &PersistConfig, mode: ShardMetricsExport) {
3944        let mut updates = ConfigUpdates::default();
3945        updates.add(&SHARD_METRICS, mode.as_str());
3946        cfg.apply_from(&updates);
3947    }
3948
3949    fn family_names(families: &[MetricFamily]) -> BTreeSet<String> {
3950        families.iter().map(|f| f.name().to_string()).collect()
3951    }
3952
3953    /// The families that stay in every mode: the shard counts and the
3954    /// process-level counters the per-shard counters roll up into.
3955    fn always_exported() -> BTreeSet<String> {
3956        let mut names: BTreeSet<_> = ["mz_persist_shard_count", "mz_persist_stale_shard_count"]
3957            .into_iter()
3958            .map(String::from)
3959            .collect();
3960        names.extend(PER_SHARD_FAMILIES.iter().filter_map(|f| match f.aggregate {
3961            ShardAggregate::Counter { total } => Some(total.to_string()),
3962            _ => None,
3963        }));
3964        names
3965    }
3966
3967    #[mz_ore::test]
3968    fn shard_metrics_modes_round_trip() {
3969        for mode in ShardMetricsExport::ALL {
3970            assert_eq!(ShardMetricsExport::parse(mode.as_str()), Some(mode));
3971        }
3972        assert_eq!(
3973            ShardMetricsExport::parse(SHARD_METRICS.default()),
3974            Some(ShardMetricsExport::default())
3975        );
3976        assert_eq!(ShardMetricsExport::parse("not_a_mode"), None);
3977    }
3978
3979    #[mz_ore::test]
3980    fn unknown_shard_metrics_mode_exports_the_default() {
3981        let (cfg, registry, _shards) = metrics_with_shards(3);
3982        let summary = family_names(&registry.gather());
3983        let invalid =
3984            |families: &[MetricFamily]| scalar(families, "mz_persist_shard_metrics_mode_invalid");
3985
3986        let mut updates = ConfigUpdates::default();
3987        updates.add(&SHARD_METRICS, "summry");
3988        cfg.apply_from(&updates);
3989
3990        let gathered = registry.gather();
3991        assert_eq!(family_names(&gathered), summary);
3992        // Counted on every scrape while it stays unrecognized, one scrape late.
3993        assert_eq!(invalid(&gathered), 0.0);
3994        assert_eq!(invalid(&registry.gather()), 1.0);
3995
3996        set_shard_metrics(&cfg, ShardMetricsExport::Both);
3997        let gathered = registry.gather();
3998        assert!(summary.is_subset(&family_names(&gathered)));
3999        assert_eq!(invalid(&gathered), 2.0);
4000        assert_eq!(invalid(&registry.gather()), 2.0);
4001    }
4002
4003    #[mz_ore::test]
4004    fn counter_totals_are_unchanged_by_the_mode() {
4005        let (cfg, registry, shards) = metrics_with_shards(3);
4006        for (i, shard) in shards.iter().enumerate() {
4007            shard.blob_gets.inc_by(u64::cast_from(i) + 1);
4008            shard.blob_sets.inc();
4009        }
4010        let totals = |families: &[MetricFamily]| -> BTreeMap<&'static str, f64> {
4011            PER_SHARD_FAMILIES
4012                .iter()
4013                .filter_map(|f| match f.aggregate {
4014                    ShardAggregate::Counter { total } => Some((total, scalar(families, total))),
4015                    _ => None,
4016                })
4017                .collect()
4018        };
4019        let both = registry.gather();
4020        assert_eq!(totals(&both)["mz_persist_blob_gets"], 6.0);
4021
4022        // The modes drop the per-shard families. The totals that replace
4023        // them don't move.
4024        for mode in ShardMetricsExport::ALL {
4025            set_shard_metrics(&cfg, mode);
4026            assert_eq!(totals(&registry.gather()), totals(&both), "{mode:?}");
4027        }
4028    }
4029
4030    #[mz_ore::test]
4031    fn each_shard_metrics_mode_exports_its_families() {
4032        let (cfg, registry, _shards) = metrics_with_shards(3);
4033        set_shard_metrics(&cfg, ShardMetricsExport::Both);
4034        let both = family_names(&registry.gather());
4035        let per_shard: BTreeSet<_> = PER_SHARD_FAMILIES
4036            .iter()
4037            .map(|f| f.name.to_string())
4038            .collect();
4039        let summary: BTreeSet<_> = both
4040            .iter()
4041            .filter(|n| {
4042                n.ends_with("_percentile")
4043                    || n.ends_with("_sum")
4044                    || n.ends_with("_topk")
4045                    || *n == "mz_persist_batch_part_version_count"
4046            })
4047            .cloned()
4048            .collect();
4049        let always = always_exported();
4050        assert!(both.is_superset(&per_shard));
4051        assert!(both.is_superset(&always));
4052        assert!(!summary.is_empty());
4053        assert!(always.is_disjoint(&per_shard) && always.is_disjoint(&summary));
4054
4055        // The mode is read on every gather, so switching it needs no new
4056        // `Metrics`. `ALL` ends with `Both`, which restores everything.
4057        for mode in ShardMetricsExport::ALL {
4058            set_shard_metrics(&cfg, mode);
4059            let mut expected = both.clone();
4060            if !mode.per_shard() {
4061                expected.retain(|n| !per_shard.contains(n));
4062            }
4063            if !mode.summary() {
4064                expected.retain(|n| !summary.contains(n));
4065            }
4066            assert!(expected.is_superset(&always), "{mode:?}");
4067            assert_eq!(family_names(&registry.gather()), expected, "{mode:?}");
4068        }
4069    }
4070
4071    #[mz_ore::test]
4072    #[cfg_attr(miri, ignore)] // too slow
4073    fn summary_scrape_is_flat_in_shard_count() {
4074        let scrape = |n: u64, mode: ShardMetricsExport| {
4075            let (cfg, registry, _shards) = metrics_with_shards(n);
4076            set_shard_metrics(&cfg, mode);
4077            let families = registry.gather();
4078            let series: usize = families.iter().map(|f| f.get_metric().len()).sum();
4079            let body = prometheus::TextEncoder::new()
4080                .encode_to_string(&families)
4081                .expect("encodable");
4082            (series, body.len())
4083        };
4084        let (few_series, few_bytes) = scrape(10, ShardMetricsExport::Summary);
4085        let (many_series, many_bytes) = scrape(10_000, ShardMetricsExport::Summary);
4086        let (both_series, both_bytes) = scrape(10_000, ShardMetricsExport::Both);
4087        assert_eq!(few_series, many_series);
4088        assert!(
4089            both_series >= many_series + PER_SHARD_FAMILIES.len() * 10_000,
4090            "{both_series} vs {many_series}"
4091        );
4092        // Only the digits of the aggregated values grow with the shard count.
4093        assert!(many_bytes < few_bytes + 4096, "{many_bytes} vs {few_bytes}");
4094        assert!(
4095            both_bytes > 100 * many_bytes,
4096            "{both_bytes} vs {many_bytes}"
4097        );
4098    }
4099
4100    #[mz_ore::test]
4101    fn shard_info_lists_live_shards_when_enabled() {
4102        let (cfg, registry, mut shards) = metrics_with_shards(3);
4103        let info =
4104            |registry: &MetricsRegistry| gauge_set(&registry.gather(), SHARD_INFO_NAME, "name");
4105        let mut updates = ConfigUpdates::default();
4106        updates.add(&SHARD_INFO_METRIC, true);
4107        cfg.apply_from(&updates);
4108        // Independent of the mode, including the one that drops everything else.
4109        for mode in ShardMetricsExport::ALL {
4110            set_shard_metrics(&cfg, mode);
4111            let expected = ["s1", "s2", "s3"].map(|n| (n.to_string(), 1)).into();
4112            assert_eq!(info(&registry), expected, "{mode:?}");
4113        }
4114
4115        shards.pop();
4116        assert_eq!(info(&registry).len(), 2);
4117    }
4118
4119    fn set_enable_regex(cfg: &PersistConfig, regex: &str) {
4120        let mut updates = ConfigUpdates::default();
4121        updates.add(&PER_SHARD_METRICS_ENABLE_REGEX, regex);
4122        cfg.apply_from(&updates);
4123    }
4124
4125    /// The per-shard series in `families`, keyed by family name and labels.
4126    fn per_shard_series(
4127        families: &[MetricFamily],
4128    ) -> BTreeMap<(String, Vec<(String, String)>), prometheus::proto::Metric> {
4129        families
4130            .iter()
4131            .filter(|f| PER_SHARD_FAMILIES.iter().any(|p| p.name == f.name()))
4132            .flat_map(|f| {
4133                f.get_metric().iter().map(|m| {
4134                    let labels = m
4135                        .get_label()
4136                        .iter()
4137                        .map(|l| (l.name().to_string(), l.value().to_string()))
4138                        .collect();
4139                    ((f.name().to_string(), labels), m.clone())
4140                })
4141            })
4142            .collect()
4143    }
4144
4145    /// The `name` labels of the shards with per-shard series in `families`.
4146    fn exported_shard_names(families: &[MetricFamily]) -> BTreeSet<String> {
4147        per_shard_series(families)
4148            .into_keys()
4149            .map(|(_, labels)| {
4150                labels
4151                    .into_iter()
4152                    .find(|(k, _)| k == "name")
4153                    .expect("per-shard series have a name label")
4154                    .1
4155            })
4156            .collect()
4157    }
4158
4159    #[mz_ore::test]
4160    fn enable_regex_keeps_matching_shards_unchanged() {
4161        let (cfg, registry, shards) = metrics_with_shards(5);
4162        set_shard_metrics(&cfg, ShardMetricsExport::Both);
4163        let both = registry.gather();
4164        set_shard_metrics(&cfg, ShardMetricsExport::Summary);
4165        let summary = registry.gather();
4166        assert!(exported_shard_names(&summary).is_empty());
4167
4168        // By shard id: exactly that shard's series come back, byte for byte
4169        // as they are in `both`, so dashboards need no change.
4170        let shard_id = shards[2].shard_id.to_string();
4171        set_enable_regex(&cfg, &shard_id);
4172        let enabled = registry.gather();
4173        let expected: BTreeMap<_, _> = per_shard_series(&both)
4174            .into_iter()
4175            .filter(|((_, labels), _)| labels.contains(&("shard".into(), shard_id.clone())))
4176            .collect();
4177        assert_eq!(expected.len(), PER_SHARD_FAMILIES.len());
4178        assert_eq!(per_shard_series(&enabled), expected);
4179        assert_eq!(family_names(&enabled), {
4180            let mut names = family_names(&summary);
4181            names.extend(PER_SHARD_FAMILIES.iter().map(|f| f.name.to_string()));
4182            names
4183        });
4184
4185        // By `name` label, and several at once.
4186        set_enable_regex(&cfg, "^s2$");
4187        assert_eq!(
4188            exported_shard_names(&registry.gather()),
4189            BTreeSet::from(["s2".to_string()])
4190        );
4191        set_enable_regex(&cfg, "^s(1|4)$");
4192        assert_eq!(
4193            exported_shard_names(&registry.gather()),
4194            BTreeSet::from(["s1".to_string(), "s4".to_string()])
4195        );
4196
4197        // Empty keeps none again.
4198        set_enable_regex(&cfg, "");
4199        assert_eq!(family_names(&registry.gather()), family_names(&summary));
4200
4201        // It applies in `none` too, and not while per-shard series are on.
4202        set_enable_regex(&cfg, "^s2$");
4203        set_shard_metrics(&cfg, ShardMetricsExport::None);
4204        let gathered = registry.gather();
4205        assert_eq!(
4206            exported_shard_names(&gathered),
4207            BTreeSet::from(["s2".to_string()])
4208        );
4209        assert!(gathered.iter().all(|f| !f.name().ends_with("_percentile")));
4210        set_shard_metrics(&cfg, ShardMetricsExport::Both);
4211        assert_eq!(exported_shard_names(&registry.gather()).len(), 5);
4212    }
4213
4214    #[mz_ore::test]
4215    fn unanchored_enable_regex_over_matches() {
4216        let cfg = PersistConfig::new_for_tests();
4217        let registry = MetricsRegistry::new();
4218        let metrics = Metrics::new(&cfg, &registry);
4219        let shard = |name: &str| metrics.shards.shard(&ShardId::new(), name);
4220        // `r` is not a hex digit, so these names can't collide with the
4221        // random shard ids the second half matches against.
4222        let _named: Vec<_> = (1..=10).map(|i| shard(&format!("src{i}"))).collect();
4223        set_shard_metrics(&cfg, ShardMetricsExport::Summary);
4224
4225        // Unanchored, so a pattern meant for one shard keeps every name it
4226        // is a substring of.
4227        set_enable_regex(&cfg, "src1");
4228        assert_eq!(
4229            exported_shard_names(&registry.gather()),
4230            BTreeSet::from(["src1".to_string(), "src10".to_string()])
4231        );
4232
4233        // It is tried against `shard` and `name`, so a pattern scoped to one
4234        // shard's id also keeps any shard whose name contains that fragment.
4235        // The naming here is contrived, but a pattern loose enough to hit
4236        // both labels is what brings the shed cardinality back.
4237        let target = shard("by-id");
4238        let fragment = target.shard_id.to_string()[3..9].to_string();
4239        let decoy = shard(&format!("t-{fragment}-t"));
4240        assert!(!decoy.shard_id.to_string().contains(&fragment));
4241        set_enable_regex(&cfg, &fragment);
4242        let kept = exported_shard_names(&registry.gather());
4243        assert!(kept.contains(&target.name), "{kept:?}");
4244        assert!(kept.contains(&decoy.name), "{kept:?}");
4245    }
4246
4247    #[mz_ore::test]
4248    fn invalid_enable_regex_fails_closed() {
4249        let (cfg, registry, _shards) = metrics_with_shards(3);
4250        set_shard_metrics(&cfg, ShardMetricsExport::Summary);
4251        let summary = family_names(&registry.gather());
4252        let invalid = |families: &[MetricFamily]| {
4253            scalar(families, "mz_persist_per_shard_metrics_regex_invalid")
4254        };
4255
4256        set_enable_regex(&cfg, "(");
4257        let gathered = registry.gather();
4258        assert_eq!(family_names(&gathered), summary);
4259        // Counted on every scrape while it stays invalid, one scrape late.
4260        assert_eq!(invalid(&gathered), 0.0);
4261        assert_eq!(invalid(&registry.gather()), 1.0);
4262
4263        set_enable_regex(&cfg, "^s1$");
4264        let gathered = registry.gather();
4265        assert_eq!(
4266            exported_shard_names(&gathered),
4267            BTreeSet::from(["s1".to_string()])
4268        );
4269        assert_eq!(invalid(&gathered), 2.0);
4270        assert_eq!(invalid(&registry.gather()), 2.0);
4271    }
4272
4273    fn gauge_count(families: &[MetricFamily], name: &str) -> usize {
4274        find(families, name).map_or(0, |f| f.get_metric().len())
4275    }
4276
4277    /// The value of one series of a `_percentile` family.
4278    fn percentile(families: &[MetricFamily], name: &str, percentile: &str) -> u64 {
4279        gauge_set(families, name, "percentile")
4280            .into_iter()
4281            .find_map(|(p, v)| (p == percentile).then_some(v))
4282            .unwrap_or_else(|| panic!("no {percentile} in {name}"))
4283    }
4284
4285    #[mz_ore::test]
4286    fn shard_aggregates_fold_live_shards() {
4287        let registry = MetricsRegistry::new();
4288        let metrics = Metrics::new(&PersistConfig::new_for_tests(), &registry);
4289        let mut shards: Vec<_> = (1..=12u64)
4290            .map(|i| {
4291                let shard = metrics.shards.shard(&ShardId::new(), &format!("s{i}"));
4292                shard.usage_current_state_batches_bytes.set(i * 100);
4293                shard
4294            })
4295            .collect();
4296        shards[0].set_batch_part_versions(["v1", "v1", "v2"].into_iter());
4297        shards[1].set_batch_part_versions(["v1"].into_iter());
4298
4299        let gathered = registry.gather();
4300        assert_eq!(scalar(&gathered, "mz_persist_shard_count"), 12.0);
4301        let percentile_families = gathered
4302            .iter()
4303            .filter(|f| {
4304                f.name().starts_with("mz_persist_shard_") && f.name().ends_with("_percentile")
4305            })
4306            .count();
4307        assert_eq!(percentile_families, shard_gauges().count());
4308        for (gauge, _) in shard_gauges() {
4309            let name = format!("{gauge}_percentile");
4310            assert_eq!(gauge_count(&gathered, &name), PERCENTILES.len(), "{name}");
4311        }
4312        let percentiles = "mz_persist_shard_usage_current_state_batches_bytes_percentile";
4313        assert_eq!(percentile(&gathered, percentiles, "min"), 100);
4314        assert_eq!(percentile(&gathered, percentiles, "p50"), 600);
4315        assert_eq!(percentile(&gathered, percentiles, "max"), 1200);
4316        let top_k = "mz_persist_shard_usage_current_state_batches_bytes_topk";
4317        let expected: BTreeSet<_> = shards[2..]
4318            .iter()
4319            .map(|s| {
4320                (
4321                    s.shard_id.to_string(),
4322                    s.usage_current_state_batches_bytes.get(),
4323                )
4324            })
4325            .collect();
4326        assert_eq!(gauge_set(&gathered, top_k, "shard"), expected);
4327        assert_eq!(
4328            gauge_set(&gathered, "mz_persist_batch_part_version_count", "version"),
4329            BTreeSet::from([("v1".to_string(), 3), ("v2".to_string(), 1)])
4330        );
4331
4332        // Dropping the largest shard removes it from the next gather, and a
4333        // version no shard has anymore is not exported.
4334        let largest = shards.pop().expect("non-empty");
4335        let largest_id = largest.shard_id.to_string();
4336        drop(largest);
4337        shards[0].set_batch_part_versions(["v1"].into_iter());
4338        let gathered = registry.gather();
4339        assert_eq!(scalar(&gathered, "mz_persist_shard_count"), 11.0);
4340        assert_eq!(percentile(&gathered, percentiles, "max"), 1100);
4341        let top_k_shards = gauge_set(&gathered, top_k, "shard");
4342        assert_eq!(top_k_shards.len(), SHARD_TOP_K);
4343        assert!(top_k_shards.iter().all(|(shard, _)| *shard != largest_id));
4344        assert_eq!(
4345            gauge_set(&gathered, "mz_persist_batch_part_version_count", "version"),
4346            BTreeSet::from([("v1".to_string(), 2)])
4347        );
4348    }
4349
4350    #[mz_ore::test]
4351    fn process_counters_survive_shard_drop() {
4352        let cfg = PersistConfig::new_for_tests();
4353        set_shard_metrics(&cfg, ShardMetricsExport::Both);
4354        let registry = MetricsRegistry::new();
4355        let metrics = Metrics::new(&cfg, &registry);
4356        let a = metrics.shards.shard(&ShardId::new(), "a");
4357        let b = metrics.shards.shard(&ShardId::new(), "b");
4358        a.blob_gets.inc_by(5);
4359        b.blob_gets.inc();
4360        a.encoded_diff_size.inc_by(100);
4361
4362        let gathered = registry.gather();
4363        assert_eq!(scalar(&gathered, "mz_persist_blob_gets"), 6.0);
4364        assert_eq!(gauge_count(&gathered, "mz_persist_shard_blob_gets"), 2);
4365
4366        drop(a);
4367        let gathered = registry.gather();
4368        assert_eq!(scalar(&gathered, "mz_persist_blob_gets"), 6.0);
4369        assert_eq!(scalar(&gathered, "mz_persist_diff_size_bytes"), 100.0);
4370        assert_eq!(gauge_count(&gathered, "mz_persist_shard_blob_gets"), 1);
4371    }
4372}