cache_kit/observability.rs
1//! Observability and metrics collection for cache operations.
2//!
3//! This module provides traits and implementations for monitoring cache behavior,
4//! tracking performance metrics, and managing cache entry time-to-live (TTL) policies.
5//!
6//! # Module Overview
7//!
8//! Cache-kit separates observability into two concerns:
9//!
10//! - **Metrics (`CacheMetrics`)**: Track hits, misses, performance timing
11//! - **TTL Policies (`TtlPolicy`)**: Control how long entries remain in cache
12//!
13//! # Metrics
14//!
15//! Implement the `CacheMetrics` trait to collect cache statistics for your monitoring system:
16//!
17//! ```ignore
18//! use cache_kit::observability::CacheMetrics;
19//! use std::time::Duration;
20//!
21//! struct PrometheusMetrics;
22//!
23//! impl CacheMetrics for PrometheusMetrics {
24//! fn record_hit(&self, _key: &str, _duration: Duration) {
25//! // Update your metrics backend
26//! // counter!("cache_hits").inc();
27//! // histogram!("cache_latency").record(duration);
28//! }
29//! // ... implement other methods
30//! }
31//!
32//! // let expander = CacheExpander::new(backend)
33//! // .with_metrics(Box::new(PrometheusMetrics));
34//! ```
35//!
36//! Default behavior (if not overridden) uses `NoOpMetrics`, which logs via the `log` crate.
37//!
38//! # TTL Policies
39//!
40//! Control cache entry lifespan with flexible TTL policies:
41//!
42//! ```
43//! use cache_kit::observability::TtlPolicy;
44//! use std::time::Duration;
45//!
46//! // Fixed TTL for all entries (5 minutes)
47//! let _policy = TtlPolicy::Fixed(Duration::from_secs(300));
48//!
49//! // Different TTL per entity type
50//! let _policy = TtlPolicy::PerType(|entity_type| {
51//! match entity_type {
52//! "user" => Duration::from_secs(3600), // 1 hour
53//! "session" => Duration::from_secs(1800), // 30 minutes
54//! _ => Duration::from_secs(600), // 10 minutes default
55//! }
56//! });
57//!
58//! // let expander = CacheExpander::new(backend)
59//! // .with_ttl_policy(_policy);
60//! ```
61//!
62//! # When to Use Each TTL Policy
63//!
64//! | Policy | Use Case | Example |
65//! |--------|----------|---------|
66//! | `Default` | Let backend decide | Works with Redis default TTL |
67//! | `Fixed` | Uniform cache duration | All entries expire in 5 minutes |
68//! | `Infinite` | Never expire | Static reference data (rarely used) |
69//! | `PerType` | Type-specific expiry | Users cache 1h, sessions 30m |
70//!
71//! # Metrics Methods
72//!
73//! The `CacheMetrics` trait provides hooks for all cache lifecycle events:
74//! - `record_hit()` - Cache hit with operation duration
75//! - `record_miss()` - Cache miss with operation duration
76//! - `record_set()` - Cache write with operation duration
77//! - `record_delete()` - Cache delete with operation duration
78//! - `record_error()` - Operation failure with error message
79//!
80//! All methods receive the cache key and relevant timing/error information.
81
82use std::time::Duration;
83
84/// Trait for cache metrics collection.
85pub trait CacheMetrics: Send + Sync {
86 /// Record a cache hit.
87 fn record_hit(&self, key: &str, duration: Duration) {
88 debug!("Cache HIT: {} took {:?}", key, duration);
89 }
90
91 /// Record a cache miss.
92 fn record_miss(&self, key: &str, duration: Duration) {
93 debug!("Cache MISS: {} took {:?}", key, duration);
94 }
95
96 /// Record a cache set operation.
97 fn record_set(&self, key: &str, duration: Duration) {
98 debug!("Cache SET: {} took {:?}", key, duration);
99 }
100
101 /// Record a cache delete operation.
102 fn record_delete(&self, key: &str, duration: Duration) {
103 debug!("Cache DELETE: {} took {:?}", key, duration);
104 }
105
106 /// Record an error.
107 fn record_error(&self, key: &str, error: &str) {
108 warn!("Cache ERROR for {}: {}", key, error);
109 }
110}
111
112/// Default metrics implementation (no-op).
113#[derive(Clone, Default)]
114pub struct NoOpMetrics;
115
116impl CacheMetrics for NoOpMetrics {
117 fn record_hit(&self, _key: &str, _duration: Duration) {}
118 fn record_miss(&self, _key: &str, _duration: Duration) {}
119 fn record_set(&self, _key: &str, _duration: Duration) {}
120 fn record_delete(&self, _key: &str, _duration: Duration) {}
121 fn record_error(&self, _key: &str, _error: &str) {}
122}
123
124/// TTL (Time-to-Live) policy for cache entries.
125#[derive(Clone, Debug, Default)]
126pub enum TtlPolicy {
127 /// Use backend's default TTL
128 #[default]
129 Default,
130
131 /// Fixed duration for all entries
132 Fixed(Duration),
133
134 /// No TTL (entries live forever)
135 Infinite,
136
137 /// Custom per-type policy
138 PerType(fn(&str) -> Duration),
139}
140
141impl TtlPolicy {
142 /// Get TTL for an entity type.
143 pub fn get_ttl(&self, entity_type: &str) -> Option<Duration> {
144 match self {
145 TtlPolicy::Default => None,
146 TtlPolicy::Fixed(d) => Some(*d),
147 TtlPolicy::Infinite => None,
148 TtlPolicy::PerType(f) => Some(f(entity_type)),
149 }
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_noop_metrics() {
159 let metrics = NoOpMetrics;
160 metrics.record_hit("key", Duration::from_secs(1));
161 metrics.record_miss("key", Duration::from_secs(2));
162 }
163
164 #[test]
165 fn test_ttl_policy_default() {
166 let policy = TtlPolicy::Default;
167 assert_eq!(policy.get_ttl("any"), None);
168 }
169
170 #[test]
171 fn test_ttl_policy_fixed() {
172 let policy = TtlPolicy::Fixed(Duration::from_secs(300));
173 assert_eq!(policy.get_ttl("any"), Some(Duration::from_secs(300)));
174 }
175
176 #[test]
177 fn test_ttl_policy_per_type() {
178 let policy = TtlPolicy::PerType(|entity_type| match entity_type {
179 "employment" => Duration::from_secs(3600),
180 _ => Duration::from_secs(1800),
181 });
182
183 assert_eq!(
184 policy.get_ttl("employment"),
185 Some(Duration::from_secs(3600))
186 );
187 assert_eq!(policy.get_ttl("other"), Some(Duration::from_secs(1800)));
188 }
189}