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