kevy_embedded/metric.rs
1//! Optional push-style metric callback. In-process embed mode has no metrics
2//! endpoint, so persistence events (AOF replay on startup, AOF rewrite/
3//! compaction) are pushed to a caller-supplied sink — wire it to Prometheus,
4//! a log line, a counter, whatever. Wire it via [`crate::Config::with_metric_sink`].
5
6use std::path::PathBuf;
7#[cfg(feature = "persist")]
8use std::sync::Arc;
9
10/// What `Store::open` restored — and what it could not. The pull-style
11/// twin of [`KevyMetric::Replay`] (`Store::open_report()`), so a host can
12/// turn "the AOF lost bytes at boot" into a health-check verdict without
13/// wiring a metric sink or scraping stderr.
14#[derive(Debug, Clone, Default)]
15#[non_exhaustive]
16pub struct OpenReport {
17 /// Commands replayed from the AOF(s), summed across shards.
18 pub replayed_commands: u64,
19 /// Bytes actually replayed (the valid prefixes).
20 pub replayed_bytes: u64,
21 /// Wall-clock time of the whole startup replay, in milliseconds.
22 pub elapsed_ms: u64,
23 /// Bytes dropped past the last replayable frame, summed across shards.
24 /// Non-zero = the store recovered less than the files held.
25 pub dropped_bytes: u64,
26 /// True when any shard's replay stopped at a corrupt frame.
27 pub corrupt: bool,
28 /// Quarantine files written while repairing dropped tails (one per
29 /// affected shard).
30 pub quarantine_paths: Vec<PathBuf>,
31 /// Bytes the resync replay hopped over (corrupt regions between valid
32 /// records, summed across shards). Zero under strict replay.
33 pub resynced_bytes: u64,
34}
35
36/// A persistence event worth observing. More variants may be added; match
37/// non-exhaustively (`_ => {}`) to stay forward-compatible.
38#[cfg(feature = "persist")]
39#[derive(Debug, Clone)]
40#[non_exhaustive]
41pub enum KevyMetric {
42 /// AOF replay finished on startup. `bytes` is the AOF size replayed.
43 /// Fires once per `Store::open`, totals summed across all shards.
44 Replay {
45 /// Commands replayed from the AOF(s).
46 commands: u64,
47 /// Size in bytes of the AOF file(s) replayed (measured before replay).
48 bytes: u64,
49 /// Wall-clock time of the whole startup replay, in milliseconds.
50 elapsed_ms: u64,
51 /// Bytes past the last replayable frame, summed across shards —
52 /// dropped from the live file (and quarantined). Non-zero means the
53 /// store recovered LESS than the file held: alert on this. A
54 /// 3-day production silent-loss incident was exactly this signal
55 /// living only in stderr.
56 dropped_bytes: u64,
57 /// True when any shard's replay stopped at a corrupt frame (vs a
58 /// clean end or a partial trailing frame).
59 corrupt: bool,
60 },
61 /// An AOF rewrite (compaction) completed. `before_bytes - after_bytes` is
62 /// the space reclaimed. Fires once per rewritten shard.
63 Rewrite {
64 /// Keys written into the compacted AOF.
65 keys: u64,
66 /// AOF size in bytes before the rewrite.
67 before_bytes: u64,
68 /// AOF size in bytes after the rewrite (the compacted log).
69 after_bytes: u64,
70 /// Wall-clock time of this shard's rewrite, in milliseconds.
71 elapsed_ms: u64,
72 },
73}
74
75/// Cloneable handle to the caller's metric callback. Cheap `Arc` clone; the
76/// callback runs synchronously on whichever thread emits the event (the reaper
77/// thread for background rewrites, the opening thread for replay), so keep it
78/// fast / non-blocking.
79#[cfg(feature = "persist")]
80#[derive(Clone)]
81pub(crate) struct MetricSink(Arc<dyn Fn(KevyMetric) + Send + Sync>);
82
83#[cfg(feature = "persist")]
84impl MetricSink {
85 pub(crate) fn new(f: impl Fn(KevyMetric) + Send + Sync + 'static) -> Self {
86 MetricSink(Arc::new(f))
87 }
88
89 pub(crate) fn emit(&self, m: KevyMetric) {
90 (self.0)(m);
91 }
92}
93
94#[cfg(feature = "persist")]
95impl std::fmt::Debug for MetricSink {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 f.write_str("MetricSink(<fn>)")
98 }
99}