opendev_runtime/
debug_logger.rs1use std::path::{Path, PathBuf};
13use std::sync::Mutex;
14use std::time::Instant;
15
16use serde_json::Value;
17
18const MAX_PREVIEW_LEN: usize = 200;
20
21fn truncate_value(value: &Value) -> Value {
23 match value {
24 Value::String(s) if s.len() > MAX_PREVIEW_LEN => {
25 let total = s.len();
26 Value::String(format!("{}... ({total} chars)", &s[..MAX_PREVIEW_LEN]))
27 }
28 Value::Object(map) => {
29 let truncated: serde_json::Map<String, Value> = map
30 .iter()
31 .map(|(k, v)| (k.clone(), truncate_value(v)))
32 .collect();
33 Value::Object(truncated)
34 }
35 Value::Array(arr) => Value::Array(arr.iter().map(truncate_value).collect()),
36 other => other.clone(),
37 }
38}
39
40pub struct SessionDebugLogger {
42 inner: Option<LoggerInner>,
43}
44
45struct LoggerInner {
46 file_path: PathBuf,
47 start_time: Instant,
48 lock: Mutex<()>,
49}
50
51impl SessionDebugLogger {
52 pub fn new(session_dir: &Path, session_id: &str) -> Self {
54 let file_path = session_dir.join(format!("{session_id}.debug"));
55
56 if let Some(parent) = file_path.parent() {
58 let _ = std::fs::create_dir_all(parent);
59 }
60
61 Self {
62 inner: Some(LoggerInner {
63 file_path,
64 start_time: Instant::now(),
65 lock: Mutex::new(()),
66 }),
67 }
68 }
69
70 pub fn noop() -> Self {
72 Self { inner: None }
73 }
74
75 pub fn is_enabled(&self) -> bool {
77 self.inner.is_some()
78 }
79
80 pub fn file_path(&self) -> Option<&Path> {
82 self.inner.as_ref().map(|i| i.file_path.as_path())
83 }
84
85 pub fn log(&self, event: &str, component: &str, data: Value) {
92 let inner = match &self.inner {
93 Some(i) => i,
94 None => return,
95 };
96
97 let elapsed_ms = inner.start_time.elapsed().as_millis() as u64;
98 let ts = chrono::Utc::now().to_rfc3339();
99
100 let truncated_data = truncate_value(&data);
101
102 let entry = serde_json::json!({
103 "ts": ts,
104 "elapsed_ms": elapsed_ms,
105 "event": event,
106 "component": component,
107 "data": truncated_data,
108 });
109
110 let line = match serde_json::to_string(&entry) {
111 Ok(s) => format!("{s}\n"),
112 Err(_) => return,
113 };
114
115 let _guard = inner.lock.lock().ok();
116 let _ = std::fs::OpenOptions::new()
117 .create(true)
118 .append(true)
119 .open(&inner.file_path)
120 .and_then(|mut f| {
121 use std::io::Write;
122 f.write_all(line.as_bytes())
123 });
124 }
125}
126
127impl std::fmt::Debug for SessionDebugLogger {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 f.debug_struct("SessionDebugLogger")
130 .field("enabled", &self.is_enabled())
131 .field("file_path", &self.file_path())
132 .finish()
133 }
134}
135
136#[cfg(test)]
137#[path = "debug_logger_tests.rs"]
138mod tests;