use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::Instant;
use serde_json::Value;
const MAX_PREVIEW_LEN: usize = 200;
fn truncate_value(value: &Value) -> Value {
match value {
Value::String(s) if s.len() > MAX_PREVIEW_LEN => {
let total = s.len();
Value::String(format!("{}... ({total} chars)", &s[..MAX_PREVIEW_LEN]))
}
Value::Object(map) => {
let truncated: serde_json::Map<String, Value> = map
.iter()
.map(|(k, v)| (k.clone(), truncate_value(v)))
.collect();
Value::Object(truncated)
}
Value::Array(arr) => Value::Array(arr.iter().map(truncate_value).collect()),
other => other.clone(),
}
}
pub struct SessionDebugLogger {
inner: Option<LoggerInner>,
}
struct LoggerInner {
file_path: PathBuf,
start_time: Instant,
lock: Mutex<()>,
}
impl SessionDebugLogger {
pub fn new(session_dir: &Path, session_id: &str) -> Self {
let file_path = session_dir.join(format!("{session_id}.debug"));
if let Some(parent) = file_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
Self {
inner: Some(LoggerInner {
file_path,
start_time: Instant::now(),
lock: Mutex::new(()),
}),
}
}
pub fn noop() -> Self {
Self { inner: None }
}
pub fn is_enabled(&self) -> bool {
self.inner.is_some()
}
pub fn file_path(&self) -> Option<&Path> {
self.inner.as_ref().map(|i| i.file_path.as_path())
}
pub fn log(&self, event: &str, component: &str, data: Value) {
let inner = match &self.inner {
Some(i) => i,
None => return,
};
let elapsed_ms = inner.start_time.elapsed().as_millis() as u64;
let ts = chrono::Utc::now().to_rfc3339();
let truncated_data = truncate_value(&data);
let entry = serde_json::json!({
"ts": ts,
"elapsed_ms": elapsed_ms,
"event": event,
"component": component,
"data": truncated_data,
});
let line = match serde_json::to_string(&entry) {
Ok(s) => format!("{s}\n"),
Err(_) => return,
};
let _guard = inner.lock.lock().ok();
let _ = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&inner.file_path)
.and_then(|mut f| {
use std::io::Write;
f.write_all(line.as_bytes())
});
}
}
impl std::fmt::Debug for SessionDebugLogger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionDebugLogger")
.field("enabled", &self.is_enabled())
.field("file_path", &self.file_path())
.finish()
}
}
#[cfg(test)]
#[path = "debug_logger_tests.rs"]
mod tests;