use anyhow::{Context, Result};
use serde_json::Value as JsonValue;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
static ACP_ERROR_SINK: std::sync::OnceLock<Arc<AcpErrorSink>> = std::sync::OnceLock::new();
pub struct AcpErrorSink {
file: Mutex<Option<std::fs::File>>,
path: PathBuf,
}
impl AcpErrorSink {
fn new(path: PathBuf) -> Result<Self> {
if let Some(parent) = path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create logs directory: {:?}", parent))?;
}
}
Ok(Self {
file: Mutex::new(None),
path,
})
}
pub fn initialize() -> Result<Arc<Self>> {
let logs_dir = crate::directories::get_logs_dir()?;
let path = logs_dir.join("acp-errors.jsonl");
let sink = Arc::new(Self::new(path)?);
let _ = ACP_ERROR_SINK.set(sink.clone());
Ok(sink)
}
pub fn get_global() -> Option<Arc<Self>> {
ACP_ERROR_SINK.get().cloned()
}
pub fn log_error_simple(&self, error: &str) -> Result<()> {
let timestamp = chrono::Utc::now().to_rfc3339();
let session_id = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
.unwrap_or_else(|| "unknown".to_string());
let entry = serde_json::json!({
"timestamp": timestamp,
"session_id": session_id,
"error": error
});
self.write_entry(&entry)
}
fn write_entry(&self, entry: &JsonValue) -> Result<()> {
let mut file_guard = self.file.lock().unwrap();
if file_guard.is_none() {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)
.with_context(|| format!("Failed to open ACP error log: {:?}", self.path))?;
*file_guard = Some(file);
}
let file = file_guard.as_mut().unwrap();
let content =
serde_json::to_string(entry).with_context(|| "Failed to serialize error entry")?;
writeln!(file, "{}", content).with_context(|| "Failed to write error entry")?;
file.flush().with_context(|| "Failed to flush error log")?;
Ok(())
}
}
impl Drop for AcpErrorSink {
fn drop(&mut self) {
if let Ok(mut file_guard) = self.file.lock() {
if let Some(file) = file_guard.as_mut() {
let _ = file.flush();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_acp_error_sink_path() {
let dir = tempdir().unwrap();
let logs_dir = dir.path().join("logs");
std::fs::create_dir_all(&logs_dir).unwrap();
let path = logs_dir.join("acp-errors.jsonl");
let sink = AcpErrorSink::new(path.clone()).unwrap();
sink.log_error_simple("Test error").unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("Test error"));
}
}