use chrono::{Local, Utc};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs::{self, OpenOptions};
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex;
use super::TraceRecord;
use crate::storage::get_traces_dir;
#[derive(Clone)]
pub struct TraceWriter {
base_dir: PathBuf,
current_file: Arc<Mutex<Option<CurrentFile>>>,
}
struct CurrentFile {
date: String,
path: PathBuf,
}
impl TraceWriter {
pub fn new(workspace_root: impl AsRef<Path>) -> Self {
let workspace_str = workspace_root.as_ref().to_string_lossy().to_string();
let base_dir = get_traces_dir(&workspace_str);
Self {
base_dir,
current_file: Arc::new(Mutex::new(None)),
}
}
pub fn with_base_dir(base_dir: impl AsRef<Path>) -> Self {
Self {
base_dir: base_dir.as_ref().to_path_buf(),
current_file: Arc::new(Mutex::new(None)),
}
}
pub async fn append(&self, record: &TraceRecord) -> Result<(), TraceWriteError> {
let today = Local::now().format("%Y-%m-%d").to_string();
let file_path = self.get_file_path(&today).await?;
let json = serde_json::to_string(record)
.map_err(|e| TraceWriteError::Serialization(e.to_string()))?;
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&file_path)
.await
.map_err(|e| TraceWriteError::Io(e.to_string()))?;
file.write_all(json.as_bytes())
.await
.map_err(|e| TraceWriteError::Io(e.to_string()))?;
file.write_all(b"\n")
.await
.map_err(|e| TraceWriteError::Io(e.to_string()))?;
file.flush()
.await
.map_err(|e| TraceWriteError::Io(e.to_string()))?;
Ok(())
}
pub async fn append_safe(&self, record: &TraceRecord) {
if let Err(e) = self.append(record).await {
tracing::warn!("[TraceWriter] Failed to write trace: {}", e);
}
}
async fn get_file_path(&self, date: &str) -> Result<PathBuf, TraceWriteError> {
let mut current = self.current_file.lock().await;
if let Some(ref cf) = *current {
if cf.date == date {
return Ok(cf.path.clone());
}
}
let day_dir = self.base_dir.join(date);
fs::create_dir_all(&day_dir)
.await
.map_err(|e| TraceWriteError::Io(format!("Failed to create trace dir: {}", e)))?;
let datetime = Utc::now().format("%Y%m%d-%H%M%S").to_string();
let filename = format!("traces-{}.jsonl", datetime);
let file_path = day_dir.join(filename);
*current = Some(CurrentFile {
date: date.to_string(),
path: file_path.clone(),
});
Ok(file_path)
}
pub fn base_dir(&self) -> &Path {
&self.base_dir
}
}
#[derive(Debug, thiserror::Error)]
pub enum TraceWriteError {
#[error("IO error: {0}")]
Io(String),
#[error("Serialization error: {0}")]
Serialization(String),
}