skp_cache_core/traits/
metrics.rs

1//! Metrics trait for cache observability
2
3use std::time::Duration;
4
5/// Cache tier for metrics labeling
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum CacheTier {
8    /// L1 in-memory cache
9    L1Memory,
10    /// L2 Redis or distributed cache
11    L2Redis,
12}
13
14impl CacheTier {
15    /// Get tier as string label
16    pub fn as_str(&self) -> &'static str {
17        match self {
18            CacheTier::L1Memory => "l1_memory",
19            CacheTier::L2Redis => "l2_redis",
20        }
21    }
22}
23
24/// Cache operation for latency tracking
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum CacheOperation {
27    Get,
28    Set,
29    Delete,
30    Serialize,
31    Deserialize,
32    Invalidate,
33}
34
35impl CacheOperation {
36    /// Get operation as string label
37    pub fn as_str(&self) -> &'static str {
38        match self {
39            CacheOperation::Get => "get",
40            CacheOperation::Set => "set",
41            CacheOperation::Delete => "delete",
42            CacheOperation::Serialize => "serialize",
43            CacheOperation::Deserialize => "deserialize",
44            CacheOperation::Invalidate => "invalidate",
45        }
46    }
47}
48
49/// Reason for cache eviction
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum EvictionReason {
52    /// TTL expired
53    Expired,
54    /// Capacity limit reached
55    Capacity,
56    /// Explicitly invalidated
57    Invalidated,
58    /// Replaced by new value
59    Replaced,
60    /// Dependency was invalidated
61    DependencyInvalidated,
62}
63
64impl EvictionReason {
65    /// Get reason as string label
66    pub fn as_str(&self) -> &'static str {
67        match self {
68            EvictionReason::Expired => "expired",
69            EvictionReason::Capacity => "capacity",
70            EvictionReason::Invalidated => "invalidated",
71            EvictionReason::Replaced => "replaced",
72            EvictionReason::DependencyInvalidated => "dependency",
73        }
74    }
75}
76
77/// Trait for cache metrics/observability
78///
79/// Implement this to integrate with your metrics system (Prometheus, StatsD, etc.)
80pub trait CacheMetrics: Send + Sync + 'static {
81    /// Record a cache hit
82    fn record_hit(&self, key: &str, tier: CacheTier);
83
84    /// Record a cache miss
85    fn record_miss(&self, key: &str);
86
87    /// Record a stale hit (served stale while revalidating)
88    fn record_stale_hit(&self, key: &str);
89
90    /// Record operation latency
91    fn record_latency(&self, operation: CacheOperation, duration: Duration);
92
93    /// Record an eviction
94    fn record_eviction(&self, reason: EvictionReason);
95
96    /// Record cache size
97    fn record_size(&self, size: usize, memory_bytes: usize);
98}
99
100/// No-op metrics implementation (default)
101///
102/// Zero overhead when metrics are not needed.
103#[derive(Debug, Clone, Copy, Default)]
104pub struct NoopMetrics;
105
106impl CacheMetrics for NoopMetrics {
107    #[inline]
108    fn record_hit(&self, _key: &str, _tier: CacheTier) {}
109
110    #[inline]
111    fn record_miss(&self, _key: &str) {}
112
113    #[inline]
114    fn record_stale_hit(&self, _key: &str) {}
115
116    #[inline]
117    fn record_latency(&self, _operation: CacheOperation, _duration: Duration) {}
118
119    #[inline]
120    fn record_eviction(&self, _reason: EvictionReason) {}
121
122    #[inline]
123    fn record_size(&self, _size: usize, _memory_bytes: usize) {}
124}
125
126/// Metrics adapter using the `metrics` crate
127///
128/// Integrates with Prometheus, StatsD, and other exporters via the `metrics` ecosystem.
129///
130/// # Example
131/// ```ignore
132/// use skp_cache_core::MetricsCrateAdapter;
133/// 
134/// // Set up a metrics recorder (e.g., prometheus_exporter)
135/// // metrics::set_global_recorder(recorder);
136///
137/// let metrics = MetricsCrateAdapter::new("skp_cache");
138/// // Emits: skp_cache_hits_total, skp_cache_misses_total, etc.
139/// ```
140#[cfg(feature = "metrics")]
141#[derive(Debug, Clone)]
142pub struct MetricsCrateAdapter {
143    prefix: String,
144}
145
146#[cfg(feature = "metrics")]
147impl MetricsCrateAdapter {
148    /// Create a new adapter with the given metric name prefix
149    pub fn new(prefix: impl Into<String>) -> Self {
150        Self {
151            prefix: prefix.into(),
152        }
153    }
154
155    fn metric_name(&self, name: &str) -> String {
156        format!("{}_{}", self.prefix, name)
157    }
158}
159
160#[cfg(feature = "metrics")]
161impl CacheMetrics for MetricsCrateAdapter {
162    fn record_hit(&self, _key: &str, tier: CacheTier) {
163        metrics::counter!(self.metric_name("hits_total"), "tier" => tier.as_str()).increment(1);
164    }
165
166    fn record_miss(&self, _key: &str) {
167        metrics::counter!(self.metric_name("misses_total")).increment(1);
168    }
169
170    fn record_stale_hit(&self, _key: &str) {
171        metrics::counter!(self.metric_name("stale_hits_total")).increment(1);
172    }
173
174    fn record_latency(&self, operation: CacheOperation, duration: Duration) {
175        metrics::histogram!(
176            self.metric_name("operation_duration_seconds"),
177            "operation" => operation.as_str()
178        )
179        .record(duration.as_secs_f64());
180    }
181
182    fn record_eviction(&self, reason: EvictionReason) {
183        metrics::counter!(
184            self.metric_name("evictions_total"),
185            "reason" => reason.as_str()
186        )
187        .increment(1);
188    }
189
190    fn record_size(&self, size: usize, memory_bytes: usize) {
191        metrics::gauge!(self.metric_name("entries")).set(size as f64);
192        metrics::gauge!(self.metric_name("memory_bytes")).set(memory_bytes as f64);
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_tier_as_str() {
202        assert_eq!(CacheTier::L1Memory.as_str(), "l1_memory");
203        assert_eq!(CacheTier::L2Redis.as_str(), "l2_redis");
204    }
205
206    #[test]
207    fn test_operation_as_str() {
208        assert_eq!(CacheOperation::Get.as_str(), "get");
209        assert_eq!(CacheOperation::Set.as_str(), "set");
210    }
211
212    #[test]
213    fn test_eviction_reason_as_str() {
214        assert_eq!(EvictionReason::Expired.as_str(), "expired");
215        assert_eq!(EvictionReason::Capacity.as_str(), "capacity");
216    }
217
218    #[test]
219    fn test_noop_metrics() {
220        let metrics = NoopMetrics;
221        // Just verify these don't panic
222        metrics.record_hit("key", CacheTier::L1Memory);
223        metrics.record_miss("key");
224        metrics.record_latency(CacheOperation::Get, Duration::from_millis(1));
225    }
226}
227