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}