1#[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#[derive(Debug, Clone)]
21pub struct MLLoggingConfig {
22 pub enabled: bool,
24
25 pub sample_rate: f64,
27
28 pub hash_keys: bool,
30
31 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 pub fn new() -> Self {
49 Self::default()
50 }
51
52 pub fn with_enabled(mut self, enabled: bool) -> Self {
54 self.enabled = enabled;
55 self
56 }
57
58 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 pub fn with_hash_keys(mut self, hash: bool) -> Self {
66 self.hash_keys = hash;
67 self
68 }
69
70 pub fn with_target(mut self, target: impl Into<String>) -> Self {
72 self.target = target.into();
73 self
74 }
75
76 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#[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 Hit,
100 Miss,
102 StaleHit,
104 Store,
106 Invalidate,
108 TagInvalidate,
110 TierPromote,
112 AdminAccess,
114}
115
116#[cfg(feature = "serde")]
122#[derive(Debug, Clone)]
123pub struct CacheEvent {
124 pub timestamp: SystemTime,
126
127 pub event_type: CacheEventType,
129
130 pub request_id: RequestId,
132
133 pub key: String,
135
136 pub method: Option<Method>,
138
139 pub uri: Option<Uri>,
141
142 pub version: Option<Version>,
144
145 pub status: Option<StatusCode>,
147
148 pub hit: bool,
150
151 pub latency_us: Option<u64>,
153
154 pub size_bytes: Option<usize>,
156
157 pub ttl_seconds: Option<u64>,
159
160 pub tags: Option<Vec<String>>,
162
163 pub tier: Option<String>,
165
166 pub promoted: bool,
168
169 pub metadata: serde_json::Value,
171}
172
173#[cfg(feature = "serde")]
174impl CacheEvent {
175 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 pub fn with_method(mut self, method: Method) -> Self {
199 self.method = Some(method);
200 self
201 }
202
203 pub fn with_uri(mut self, uri: Uri) -> Self {
205 self.uri = Some(uri);
206 self
207 }
208
209 pub fn with_version(mut self, version: Version) -> Self {
211 self.version = Some(version);
212 self
213 }
214
215 pub fn with_status(mut self, status: StatusCode) -> Self {
217 self.status = Some(status);
218 self
219 }
220
221 pub fn with_hit(mut self, hit: bool) -> Self {
223 self.hit = hit;
224 self
225 }
226
227 pub fn with_latency(mut self, latency: Duration) -> Self {
229 self.latency_us = Some(latency.as_micros() as u64);
230 self
231 }
232
233 pub fn with_size(mut self, size: usize) -> Self {
235 self.size_bytes = Some(size);
236 self
237 }
238
239 pub fn with_ttl(mut self, ttl: Duration) -> Self {
241 self.ttl_seconds = Some(ttl.as_secs());
242 self
243 }
244
245 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
247 self.tags = Some(tags);
248 self
249 }
250
251 pub fn with_tier(mut self, tier: impl Into<String>) -> Self {
253 self.tier = Some(tier.into());
254 self
255 }
256
257 pub fn with_promoted(mut self, promoted: bool) -> Self {
259 self.promoted = promoted;
260 self
261 }
262
263 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
265 self.metadata = metadata;
266 self
267 }
268
269 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 timestamp = self
282 .timestamp
283 .duration_since(SystemTime::UNIX_EPOCH)
284 .unwrap_or_default();
285
286 let log_data = json!({
287 "timestamp": format!("{}.{:03}Z",
288 chrono::DateTime::<chrono::Utc>::from(self.timestamp)
289 .format("%Y-%m-%dT%H:%M:%S"),
290 timestamp.subsec_millis()
291 ),
292 "level": "info",
293 "event": format!("{:?}", self.event_type).to_lowercase(),
294 "request_id": self.request_id.as_str(),
295 "key": key,
296 "method": self.method.as_ref().map(|m| m.as_str()),
297 "uri": self.uri.as_ref().map(|u| u.to_string()),
298 "version": self.version.as_ref().map(|v| format!("{:?}", v)),
299 "status": self.status.as_ref().map(|s| s.as_u16()),
300 "hit": self.hit,
301 "latency_us": self.latency_us,
302 "size_bytes": self.size_bytes,
303 "ttl_seconds": self.ttl_seconds,
304 "tags": self.tags,
305 "tier": self.tier,
306 "promoted": self.promoted,
307 "metadata": self.metadata,
308 });
309
310 #[cfg(feature = "tracing")]
311 {
312 tracing::info!(
314 target: "tower_http_cache::ml",
315 event = %log_data
316 );
317 }
318
319 #[cfg(not(feature = "tracing"))]
320 {
321 let _ = config; println!("{}", log_data);
324 }
325 }
326}
327
328pub fn hash_key(key: &str) -> String {
330 let mut hasher = Sha256::new();
331 hasher.update(key.as_bytes());
332 let result = hasher.finalize();
333 hex::encode(result)
334}
335
336#[cfg(feature = "serde")]
338pub fn log_cache_operation(
339 config: &MLLoggingConfig,
340 event_type: CacheEventType,
341 request_id: RequestId,
342 key: String,
343) {
344 if !config.enabled {
345 return;
346 }
347
348 let event = CacheEvent::new(event_type, request_id, key);
349 event.log(config);
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn ml_logging_config_default() {
358 let config = MLLoggingConfig::default();
359 assert!(!config.enabled);
360 assert_eq!(config.sample_rate, 1.0);
361 assert!(config.hash_keys);
362 }
363
364 #[test]
365 fn ml_logging_config_builder() {
366 let config = MLLoggingConfig::new()
367 .with_enabled(true)
368 .with_sample_rate(0.5)
369 .with_hash_keys(false)
370 .with_target("custom::target");
371
372 assert!(config.enabled);
373 assert_eq!(config.sample_rate, 0.5);
374 assert!(!config.hash_keys);
375 assert_eq!(config.target, "custom::target");
376 }
377
378 #[test]
379 fn sample_rate_clamped() {
380 let config = MLLoggingConfig::new().with_sample_rate(1.5);
381 assert_eq!(config.sample_rate, 1.0);
382
383 let config = MLLoggingConfig::new().with_sample_rate(-0.5);
384 assert_eq!(config.sample_rate, 0.0);
385 }
386
387 #[test]
388 fn should_sample_when_disabled() {
389 let config = MLLoggingConfig::new().with_enabled(false);
390 assert!(!config.should_sample());
391 }
392
393 #[test]
394 fn should_sample_when_rate_is_one() {
395 let config = MLLoggingConfig::new()
396 .with_enabled(true)
397 .with_sample_rate(1.0);
398 assert!(config.should_sample());
399 }
400
401 #[test]
402 fn hash_key_consistent() {
403 let key = "/api/users/123";
404 let hash1 = hash_key(key);
405 let hash2 = hash_key(key);
406 assert_eq!(hash1, hash2);
407 assert_ne!(hash1, key);
408 assert_eq!(hash1.len(), 64); }
410
411 #[cfg(feature = "serde")]
412 #[test]
413 fn cache_event_builder() {
414 let request_id = RequestId::new();
415 let event = CacheEvent::new(CacheEventType::Hit, request_id.clone(), "/test".to_string())
416 .with_method(Method::GET)
417 .with_status(StatusCode::OK)
418 .with_hit(true)
419 .with_latency(Duration::from_micros(150))
420 .with_size(1024)
421 .with_ttl(Duration::from_secs(300))
422 .with_tags(vec!["user:123".to_string()])
423 .with_tier("l1")
424 .with_promoted(false);
425
426 assert_eq!(event.method, Some(Method::GET));
427 assert_eq!(event.status, Some(StatusCode::OK));
428 assert!(event.hit);
429 assert_eq!(event.latency_us, Some(150));
430 assert_eq!(event.size_bytes, Some(1024));
431 assert_eq!(event.ttl_seconds, Some(300));
432 assert_eq!(event.tags, Some(vec!["user:123".to_string()]));
433 assert_eq!(event.tier, Some("l1".to_string()));
434 assert!(!event.promoted);
435 }
436
437 #[cfg(feature = "serde")]
438 #[test]
439 fn cache_event_log_disabled() {
440 let config = MLLoggingConfig::new().with_enabled(false);
441 let request_id = RequestId::new();
442 let event = CacheEvent::new(CacheEventType::Hit, request_id, "/test".to_string());
443
444 event.log(&config);
446 }
447
448 #[cfg(feature = "serde")]
449 #[test]
450 fn cache_event_log_with_hashing() {
451 let config = MLLoggingConfig::new()
452 .with_enabled(true)
453 .with_hash_keys(true);
454 let request_id = RequestId::new();
455 let event = CacheEvent::new(CacheEventType::Hit, request_id, "/api/secret".to_string());
456
457 event.log(&config);
459 }
460
461 #[cfg(feature = "serde")]
462 #[test]
463 fn log_cache_operation_helper() {
464 let config = MLLoggingConfig::new().with_enabled(true);
465 let request_id = RequestId::new();
466
467 log_cache_operation(
469 &config,
470 CacheEventType::Miss,
471 request_id,
472 "/test".to_string(),
473 );
474 }
475}