1use crate::request_id::RequestId;
8use http::{Method, StatusCode, Uri, Version};
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11use sha2::{Digest, Sha256};
12use std::time::{Duration, SystemTime};
13
14#[derive(Debug, Clone)]
16pub struct MLLoggingConfig {
17 pub enabled: bool,
19
20 pub sample_rate: f64,
22
23 pub hash_keys: bool,
25
26 pub target: String,
28}
29
30impl Default for MLLoggingConfig {
31 fn default() -> Self {
32 Self {
33 enabled: false,
34 sample_rate: 1.0,
35 hash_keys: true,
36 target: "tower_http_cache::ml".to_string(),
37 }
38 }
39}
40
41impl MLLoggingConfig {
42 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn with_enabled(mut self, enabled: bool) -> Self {
49 self.enabled = enabled;
50 self
51 }
52
53 pub fn with_sample_rate(mut self, rate: f64) -> Self {
55 self.sample_rate = rate.clamp(0.0, 1.0);
56 self
57 }
58
59 pub fn with_hash_keys(mut self, hash: bool) -> Self {
61 self.hash_keys = hash;
62 self
63 }
64
65 pub fn with_target(mut self, target: impl Into<String>) -> Self {
67 self.target = target.into();
68 self
69 }
70
71 pub fn should_sample(&self) -> bool {
73 if !self.enabled {
74 return false;
75 }
76 if self.sample_rate >= 1.0 {
77 return true;
78 }
79 use std::collections::hash_map::RandomState;
80 use std::hash::BuildHasher;
81 let hasher = RandomState::new();
82
83 let random = (hasher.hash_one(std::time::SystemTime::now()) as f64) / (u64::MAX as f64);
84 random < self.sample_rate
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum CacheEventType {
92 Hit,
94 Miss,
96 StaleHit,
98 Store,
100 Invalidate,
102 TagInvalidate,
104 TierPromote,
106 AdminAccess,
108}
109
110#[derive(Debug, Clone)]
112pub struct CacheEvent {
113 pub timestamp: SystemTime,
115
116 pub event_type: CacheEventType,
118
119 pub request_id: RequestId,
121
122 pub key: String,
124
125 pub method: Option<Method>,
127
128 pub uri: Option<Uri>,
130
131 pub version: Option<Version>,
133
134 pub status: Option<StatusCode>,
136
137 pub hit: bool,
139
140 pub latency_us: Option<u64>,
142
143 pub size_bytes: Option<usize>,
145
146 pub ttl_seconds: Option<u64>,
148
149 pub tags: Option<Vec<String>>,
151
152 pub tier: Option<String>,
154
155 pub promoted: bool,
157
158 pub metadata: serde_json::Value,
160}
161
162impl CacheEvent {
163 pub fn new(event_type: CacheEventType, request_id: RequestId, key: String) -> Self {
165 Self {
166 timestamp: SystemTime::now(),
167 event_type,
168 request_id,
169 key,
170 method: None,
171 uri: None,
172 version: None,
173 status: None,
174 hit: false,
175 latency_us: None,
176 size_bytes: None,
177 ttl_seconds: None,
178 tags: None,
179 tier: None,
180 promoted: false,
181 metadata: json!({}),
182 }
183 }
184
185 pub fn with_method(mut self, method: Method) -> Self {
187 self.method = Some(method);
188 self
189 }
190
191 pub fn with_uri(mut self, uri: Uri) -> Self {
193 self.uri = Some(uri);
194 self
195 }
196
197 pub fn with_version(mut self, version: Version) -> Self {
199 self.version = Some(version);
200 self
201 }
202
203 pub fn with_status(mut self, status: StatusCode) -> Self {
205 self.status = Some(status);
206 self
207 }
208
209 pub fn with_hit(mut self, hit: bool) -> Self {
211 self.hit = hit;
212 self
213 }
214
215 pub fn with_latency(mut self, latency: Duration) -> Self {
217 self.latency_us = Some(latency.as_micros() as u64);
218 self
219 }
220
221 pub fn with_size(mut self, size: usize) -> Self {
223 self.size_bytes = Some(size);
224 self
225 }
226
227 pub fn with_ttl(mut self, ttl: Duration) -> Self {
229 self.ttl_seconds = Some(ttl.as_secs());
230 self
231 }
232
233 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
235 self.tags = Some(tags);
236 self
237 }
238
239 pub fn with_tier(mut self, tier: impl Into<String>) -> Self {
241 self.tier = Some(tier.into());
242 self
243 }
244
245 pub fn with_promoted(mut self, promoted: bool) -> Self {
247 self.promoted = promoted;
248 self
249 }
250
251 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
253 self.metadata = metadata;
254 self
255 }
256
257 pub fn log(&self, config: &MLLoggingConfig) {
259 if !config.should_sample() {
260 return;
261 }
262
263 let key = if config.hash_keys {
264 hash_key(&self.key)
265 } else {
266 self.key.clone()
267 };
268
269 let timestamp = self
270 .timestamp
271 .duration_since(SystemTime::UNIX_EPOCH)
272 .unwrap_or_default();
273
274 let log_data = json!({
275 "timestamp": format!("{}.{:03}Z",
276 chrono::DateTime::<chrono::Utc>::from(self.timestamp)
277 .format("%Y-%m-%dT%H:%M:%S"),
278 timestamp.subsec_millis()
279 ),
280 "level": "info",
281 "event": format!("{:?}", self.event_type).to_lowercase(),
282 "request_id": self.request_id.as_str(),
283 "key": key,
284 "method": self.method.as_ref().map(|m| m.as_str()),
285 "uri": self.uri.as_ref().map(|u| u.to_string()),
286 "version": self.version.as_ref().map(|v| format!("{:?}", v)),
287 "status": self.status.as_ref().map(|s| s.as_u16()),
288 "hit": self.hit,
289 "latency_us": self.latency_us,
290 "size_bytes": self.size_bytes,
291 "ttl_seconds": self.ttl_seconds,
292 "tags": self.tags,
293 "tier": self.tier,
294 "promoted": self.promoted,
295 "metadata": self.metadata,
296 });
297
298 #[cfg(feature = "tracing")]
299 {
300 tracing::info!(
302 target: "tower_http_cache::ml",
303 event = %log_data
304 );
305 }
306
307 #[cfg(not(feature = "tracing"))]
308 {
309 let _ = config; println!("{}", log_data);
312 }
313 }
314}
315
316pub fn hash_key(key: &str) -> String {
318 let mut hasher = Sha256::new();
319 hasher.update(key.as_bytes());
320 let result = hasher.finalize();
321 hex::encode(result)
322}
323
324pub fn log_cache_operation(
326 config: &MLLoggingConfig,
327 event_type: CacheEventType,
328 request_id: RequestId,
329 key: String,
330) {
331 if !config.enabled {
332 return;
333 }
334
335 let event = CacheEvent::new(event_type, request_id, key);
336 event.log(config);
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn ml_logging_config_default() {
345 let config = MLLoggingConfig::default();
346 assert!(!config.enabled);
347 assert_eq!(config.sample_rate, 1.0);
348 assert!(config.hash_keys);
349 }
350
351 #[test]
352 fn ml_logging_config_builder() {
353 let config = MLLoggingConfig::new()
354 .with_enabled(true)
355 .with_sample_rate(0.5)
356 .with_hash_keys(false)
357 .with_target("custom::target");
358
359 assert!(config.enabled);
360 assert_eq!(config.sample_rate, 0.5);
361 assert!(!config.hash_keys);
362 assert_eq!(config.target, "custom::target");
363 }
364
365 #[test]
366 fn sample_rate_clamped() {
367 let config = MLLoggingConfig::new().with_sample_rate(1.5);
368 assert_eq!(config.sample_rate, 1.0);
369
370 let config = MLLoggingConfig::new().with_sample_rate(-0.5);
371 assert_eq!(config.sample_rate, 0.0);
372 }
373
374 #[test]
375 fn should_sample_when_disabled() {
376 let config = MLLoggingConfig::new().with_enabled(false);
377 assert!(!config.should_sample());
378 }
379
380 #[test]
381 fn should_sample_when_rate_is_one() {
382 let config = MLLoggingConfig::new()
383 .with_enabled(true)
384 .with_sample_rate(1.0);
385 assert!(config.should_sample());
386 }
387
388 #[test]
389 fn hash_key_consistent() {
390 let key = "/api/users/123";
391 let hash1 = hash_key(key);
392 let hash2 = hash_key(key);
393 assert_eq!(hash1, hash2);
394 assert_ne!(hash1, key);
395 assert_eq!(hash1.len(), 64); }
397
398 #[test]
399 fn cache_event_builder() {
400 let request_id = RequestId::new();
401 let event = CacheEvent::new(CacheEventType::Hit, request_id.clone(), "/test".to_string())
402 .with_method(Method::GET)
403 .with_status(StatusCode::OK)
404 .with_hit(true)
405 .with_latency(Duration::from_micros(150))
406 .with_size(1024)
407 .with_ttl(Duration::from_secs(300))
408 .with_tags(vec!["user:123".to_string()])
409 .with_tier("l1")
410 .with_promoted(false);
411
412 assert_eq!(event.method, Some(Method::GET));
413 assert_eq!(event.status, Some(StatusCode::OK));
414 assert!(event.hit);
415 assert_eq!(event.latency_us, Some(150));
416 assert_eq!(event.size_bytes, Some(1024));
417 assert_eq!(event.ttl_seconds, Some(300));
418 assert_eq!(event.tags, Some(vec!["user:123".to_string()]));
419 assert_eq!(event.tier, Some("l1".to_string()));
420 assert!(!event.promoted);
421 }
422
423 #[test]
424 fn cache_event_log_disabled() {
425 let config = MLLoggingConfig::new().with_enabled(false);
426 let request_id = RequestId::new();
427 let event = CacheEvent::new(CacheEventType::Hit, request_id, "/test".to_string());
428
429 event.log(&config);
431 }
432
433 #[test]
434 fn cache_event_log_with_hashing() {
435 let config = MLLoggingConfig::new()
436 .with_enabled(true)
437 .with_hash_keys(true);
438 let request_id = RequestId::new();
439 let event = CacheEvent::new(CacheEventType::Hit, request_id, "/api/secret".to_string());
440
441 event.log(&config);
443 }
444
445 #[test]
446 fn log_cache_operation_helper() {
447 let config = MLLoggingConfig::new().with_enabled(true);
448 let request_id = RequestId::new();
449
450 log_cache_operation(
452 &config,
453 CacheEventType::Miss,
454 request_id,
455 "/test".to_string(),
456 );
457 }
458}