Skip to main content

tower_http_cache/
logging.rs

1//! ML-ready structured logging for cache operations.
2//!
3//! This module provides comprehensive structured logging suitable for
4//! machine learning training and analysis. All cache operations emit
5//! JSON-formatted logs with rich metadata for correlation and analysis.
6
7#[cfg(feature = "serde")]
8use crate::request_id::RequestId;
9#[cfg(feature = "serde")]
10use http::{Method, StatusCode, Uri, Version};
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13#[cfg(feature = "serde")]
14use serde_json::json;
15use sha2::{Digest, Sha256};
16#[cfg(feature = "serde")]
17use std::time::{Duration, SystemTime};
18
19/// Configuration for ML-ready structured logging.
20#[derive(Debug, Clone)]
21pub struct MLLoggingConfig {
22    /// Enable ML-ready structured logging
23    pub enabled: bool,
24
25    /// Sample rate (1.0 = all requests, 0.1 = 10%)
26    pub sample_rate: f64,
27
28    /// Hash cache keys for privacy (recommended for production)
29    pub hash_keys: bool,
30
31    /// Target for structured logs (defaults to "tower_http_cache::ml")
32    pub target: String,
33}
34
35impl Default for MLLoggingConfig {
36    fn default() -> Self {
37        Self {
38            enabled: false,
39            sample_rate: 1.0,
40            hash_keys: true,
41            target: "tower_http_cache::ml".to_string(),
42        }
43    }
44}
45
46impl MLLoggingConfig {
47    /// Creates a new ML logging configuration with default settings.
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Enables ML logging.
53    pub fn with_enabled(mut self, enabled: bool) -> Self {
54        self.enabled = enabled;
55        self
56    }
57
58    /// Sets the sample rate (0.0 to 1.0).
59    pub fn with_sample_rate(mut self, rate: f64) -> Self {
60        self.sample_rate = rate.clamp(0.0, 1.0);
61        self
62    }
63
64    /// Enables or disables key hashing.
65    pub fn with_hash_keys(mut self, hash: bool) -> Self {
66        self.hash_keys = hash;
67        self
68    }
69
70    /// Sets a custom logging target.
71    pub fn with_target(mut self, target: impl Into<String>) -> Self {
72        self.target = target.into();
73        self
74    }
75
76    /// Checks if this request should be logged based on sampling rate.
77    pub fn should_sample(&self) -> bool {
78        if !self.enabled {
79            return false;
80        }
81        if self.sample_rate >= 1.0 {
82            return true;
83        }
84        use std::collections::hash_map::RandomState;
85        use std::hash::BuildHasher;
86        let hasher = RandomState::new();
87
88        let random = (hasher.hash_one(std::time::SystemTime::now()) as f64) / (u64::MAX as f64);
89        random < self.sample_rate
90    }
91}
92
93/// Types of cache events that can be logged.
94#[derive(Debug, Clone)]
95#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
96#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
97pub enum CacheEventType {
98    /// Cache lookup hit (fresh entry)
99    Hit,
100    /// Cache lookup miss
101    Miss,
102    /// Stale entry served
103    StaleHit,
104    /// Cache entry stored
105    Store,
106    /// Cache entry invalidated
107    Invalidate,
108    /// Tag-based invalidation
109    TagInvalidate,
110    /// Multi-tier cache promotion
111    TierPromote,
112    /// Admin API access
113    AdminAccess,
114}
115
116/// Structured cache event for ML training.
117/// Structured cache event, emitted as JSON.
118///
119/// Requires the `serde` feature: the `metadata` field is a
120/// [`serde_json::Value`] and the payload is emitted as JSON.
121#[cfg(feature = "serde")]
122#[derive(Debug, Clone)]
123pub struct CacheEvent {
124    /// Timestamp of the event
125    pub timestamp: SystemTime,
126
127    /// Type of cache event
128    pub event_type: CacheEventType,
129
130    /// Request ID for correlation
131    pub request_id: RequestId,
132
133    /// Cache key (may be hashed)
134    pub key: String,
135
136    /// Request method
137    pub method: Option<Method>,
138
139    /// Request URI
140    pub uri: Option<Uri>,
141
142    /// HTTP version
143    pub version: Option<Version>,
144
145    /// Response status code
146    pub status: Option<StatusCode>,
147
148    /// Whether this was a cache hit
149    pub hit: bool,
150
151    /// Operation latency in microseconds
152    pub latency_us: Option<u64>,
153
154    /// Response size in bytes
155    pub size_bytes: Option<usize>,
156
157    /// TTL in seconds
158    pub ttl_seconds: Option<u64>,
159
160    /// Cache tags associated with this entry
161    pub tags: Option<Vec<String>>,
162
163    /// Tier information (l1, l2, or None)
164    pub tier: Option<String>,
165
166    /// Whether entry was promoted between tiers
167    pub promoted: bool,
168
169    /// Additional metadata
170    pub metadata: serde_json::Value,
171}
172
173#[cfg(feature = "serde")]
174impl CacheEvent {
175    /// Creates a new cache event.
176    pub fn new(event_type: CacheEventType, request_id: RequestId, key: String) -> Self {
177        Self {
178            timestamp: SystemTime::now(),
179            event_type,
180            request_id,
181            key,
182            method: None,
183            uri: None,
184            version: None,
185            status: None,
186            hit: false,
187            latency_us: None,
188            size_bytes: None,
189            ttl_seconds: None,
190            tags: None,
191            tier: None,
192            promoted: false,
193            metadata: json!({}),
194        }
195    }
196
197    /// Sets the HTTP method.
198    pub fn with_method(mut self, method: Method) -> Self {
199        self.method = Some(method);
200        self
201    }
202
203    /// Sets the request URI.
204    pub fn with_uri(mut self, uri: Uri) -> Self {
205        self.uri = Some(uri);
206        self
207    }
208
209    /// Sets the HTTP version.
210    pub fn with_version(mut self, version: Version) -> Self {
211        self.version = Some(version);
212        self
213    }
214
215    /// Sets the response status.
216    pub fn with_status(mut self, status: StatusCode) -> Self {
217        self.status = Some(status);
218        self
219    }
220
221    /// Sets whether this was a cache hit.
222    pub fn with_hit(mut self, hit: bool) -> Self {
223        self.hit = hit;
224        self
225    }
226
227    /// Sets the operation latency.
228    pub fn with_latency(mut self, latency: Duration) -> Self {
229        self.latency_us = Some(latency.as_micros() as u64);
230        self
231    }
232
233    /// Sets the response size.
234    pub fn with_size(mut self, size: usize) -> Self {
235        self.size_bytes = Some(size);
236        self
237    }
238
239    /// Sets the TTL.
240    pub fn with_ttl(mut self, ttl: Duration) -> Self {
241        self.ttl_seconds = Some(ttl.as_secs());
242        self
243    }
244
245    /// Sets the cache tags.
246    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
247        self.tags = Some(tags);
248        self
249    }
250
251    /// Sets the tier information.
252    pub fn with_tier(mut self, tier: impl Into<String>) -> Self {
253        self.tier = Some(tier.into());
254        self
255    }
256
257    /// Sets whether the entry was promoted.
258    pub fn with_promoted(mut self, promoted: bool) -> Self {
259        self.promoted = promoted;
260        self
261    }
262
263    /// Adds custom metadata.
264    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
265        self.metadata = metadata;
266        self
267    }
268
269    /// Logs this event using the provided configuration.
270    pub fn log(&self, config: &MLLoggingConfig) {
271        if !config.should_sample() {
272            return;
273        }
274
275        let key = if config.hash_keys {
276            hash_key(&self.key)
277        } else {
278            self.key.clone()
279        };
280
281        let log_data = json!({
282            "timestamp": crate::time_fmt::format_iso8601_millis(self.timestamp),
283            "level": "info",
284            "event": format!("{:?}", self.event_type).to_lowercase(),
285            "request_id": self.request_id.as_str(),
286            "key": key,
287            "method": self.method.as_ref().map(|m| m.as_str()),
288            "uri": self.uri.as_ref().map(|u| u.to_string()),
289            "version": self.version.as_ref().map(|v| format!("{:?}", v)),
290            "status": self.status.as_ref().map(|s| s.as_u16()),
291            "hit": self.hit,
292            "latency_us": self.latency_us,
293            "size_bytes": self.size_bytes,
294            "ttl_seconds": self.ttl_seconds,
295            "tags": self.tags,
296            "tier": self.tier,
297            "promoted": self.promoted,
298            "metadata": self.metadata,
299        });
300
301        #[cfg(feature = "tracing")]
302        {
303            // Use fixed target for tracing, but include the configured target in the log data
304            tracing::info!(
305                target: "tower_http_cache::ml",
306                event = %log_data
307            );
308        }
309
310        #[cfg(not(feature = "tracing"))]
311        {
312            // Fallback to println for non-tracing builds
313            let _ = config; // suppress warning
314            println!("{}", log_data);
315        }
316    }
317}
318
319/// Hashes a cache key using SHA-256 for privacy.
320pub fn hash_key(key: &str) -> String {
321    let mut hasher = Sha256::new();
322    hasher.update(key.as_bytes());
323    let result = hasher.finalize();
324    hex::encode(result)
325}
326
327/// Helper to log a simple cache operation.
328#[cfg(feature = "serde")]
329pub fn log_cache_operation(
330    config: &MLLoggingConfig,
331    event_type: CacheEventType,
332    request_id: RequestId,
333    key: String,
334) {
335    if !config.enabled {
336        return;
337    }
338
339    let event = CacheEvent::new(event_type, request_id, key);
340    event.log(config);
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    #[test]
348    fn ml_logging_config_default() {
349        let config = MLLoggingConfig::default();
350        assert!(!config.enabled);
351        assert_eq!(config.sample_rate, 1.0);
352        assert!(config.hash_keys);
353    }
354
355    #[test]
356    fn ml_logging_config_builder() {
357        let config = MLLoggingConfig::new()
358            .with_enabled(true)
359            .with_sample_rate(0.5)
360            .with_hash_keys(false)
361            .with_target("custom::target");
362
363        assert!(config.enabled);
364        assert_eq!(config.sample_rate, 0.5);
365        assert!(!config.hash_keys);
366        assert_eq!(config.target, "custom::target");
367    }
368
369    #[test]
370    fn sample_rate_clamped() {
371        let config = MLLoggingConfig::new().with_sample_rate(1.5);
372        assert_eq!(config.sample_rate, 1.0);
373
374        let config = MLLoggingConfig::new().with_sample_rate(-0.5);
375        assert_eq!(config.sample_rate, 0.0);
376    }
377
378    #[test]
379    fn should_sample_when_disabled() {
380        let config = MLLoggingConfig::new().with_enabled(false);
381        assert!(!config.should_sample());
382    }
383
384    #[test]
385    fn should_sample_when_rate_is_one() {
386        let config = MLLoggingConfig::new()
387            .with_enabled(true)
388            .with_sample_rate(1.0);
389        assert!(config.should_sample());
390    }
391
392    #[test]
393    fn hash_key_consistent() {
394        let key = "/api/users/123";
395        let hash1 = hash_key(key);
396        let hash2 = hash_key(key);
397        assert_eq!(hash1, hash2);
398        assert_ne!(hash1, key);
399        assert_eq!(hash1.len(), 64); // SHA-256 produces 64 hex chars
400    }
401
402    #[cfg(feature = "serde")]
403    #[test]
404    fn cache_event_builder() {
405        let request_id = RequestId::new();
406        let event = CacheEvent::new(CacheEventType::Hit, request_id.clone(), "/test".to_string())
407            .with_method(Method::GET)
408            .with_status(StatusCode::OK)
409            .with_hit(true)
410            .with_latency(Duration::from_micros(150))
411            .with_size(1024)
412            .with_ttl(Duration::from_secs(300))
413            .with_tags(vec!["user:123".to_string()])
414            .with_tier("l1")
415            .with_promoted(false);
416
417        assert_eq!(event.method, Some(Method::GET));
418        assert_eq!(event.status, Some(StatusCode::OK));
419        assert!(event.hit);
420        assert_eq!(event.latency_us, Some(150));
421        assert_eq!(event.size_bytes, Some(1024));
422        assert_eq!(event.ttl_seconds, Some(300));
423        assert_eq!(event.tags, Some(vec!["user:123".to_string()]));
424        assert_eq!(event.tier, Some("l1".to_string()));
425        assert!(!event.promoted);
426    }
427
428    #[cfg(feature = "serde")]
429    #[test]
430    fn cache_event_log_disabled() {
431        let config = MLLoggingConfig::new().with_enabled(false);
432        let request_id = RequestId::new();
433        let event = CacheEvent::new(CacheEventType::Hit, request_id, "/test".to_string());
434
435        // Should not panic when logging is disabled
436        event.log(&config);
437    }
438
439    #[cfg(feature = "serde")]
440    #[test]
441    fn cache_event_log_with_hashing() {
442        let config = MLLoggingConfig::new()
443            .with_enabled(true)
444            .with_hash_keys(true);
445        let request_id = RequestId::new();
446        let event = CacheEvent::new(CacheEventType::Hit, request_id, "/api/secret".to_string());
447
448        // Should not panic when logging with hashing
449        event.log(&config);
450    }
451
452    #[cfg(feature = "serde")]
453    #[test]
454    fn log_cache_operation_helper() {
455        let config = MLLoggingConfig::new().with_enabled(true);
456        let request_id = RequestId::new();
457
458        // Should not panic
459        log_cache_operation(
460            &config,
461            CacheEventType::Miss,
462            request_id,
463            "/test".to_string(),
464        );
465    }
466}