use std::fs;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use anyhow::{Context, Result};
use chrono::Utc;
use serde::Serialize;
use tracing::subscriber::set_global_default;
use tracing_appender::non_blocking::{self, WorkerGuard};
use tracing_appender::rolling;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
use crate::config::{LogFormat, LoggingConfig};
pub struct LoggingGuards {
_log_guard: Option<WorkerGuard>,
_json_guard: Option<WorkerGuard>,
}
pub fn init_logging(logging: &LoggingConfig) -> Result<PathBuf> {
if logging.enable_logs {
fs::create_dir_all(&logging.logs_dir).with_context(|| {
format!(
"Failed to create logs directory: {}",
logging.logs_dir.display()
)
})?;
}
let console_layer = if logging.human_console {
Some(fmt::layer().with_target(false).with_level(true))
} else {
None
};
let now = Utc::now().format("%Y%m%dT%H%M%SZ");
let (plain_layer, log_guard) = if logging.enable_logs
&& (logging.log_format == LogFormat::Plain || logging.log_format == LogFormat::Both)
{
let filename = format!("run-{}.log", now);
let file_appender = rolling::never(&logging.logs_dir, &filename);
let (non_blocking, guard) = non_blocking::NonBlockingBuilder::default()
.lossy(false)
.finish(file_appender);
let layer = fmt::layer()
.with_target(false)
.with_level(true)
.with_ansi(false) .with_writer(move || non_blocking.clone());
(Some(layer), Some(guard))
} else {
(None, None)
};
let (json_layer, json_guard) = if logging.enable_logs
&& (logging.log_format == LogFormat::Json || logging.log_format == LogFormat::Both)
{
let filename = format!("run-{}.jsonl", now);
let file_appender = rolling::never(&logging.logs_dir, &filename);
let (non_blocking, guard) = non_blocking::NonBlockingBuilder::default()
.lossy(false)
.finish(file_appender);
let layer = fmt::layer()
.json()
.with_current_span(true)
.with_span_list(true)
.with_target(false)
.with_level(true)
.with_writer(move || non_blocking.clone());
(Some(layer), Some(guard))
} else {
(None, None)
};
static LOG_GUARDS: OnceLock<LoggingGuards> = OnceLock::new();
let _ = LOG_GUARDS.set(LoggingGuards {
_log_guard: log_guard,
_json_guard: json_guard,
});
let filter_directive = std::env::var("RUST_LOG").unwrap_or_else(|_| logging.level.clone());
let env_filter =
EnvFilter::try_new(&filter_directive).unwrap_or_else(|_| EnvFilter::new("info"));
let subscriber = tracing_subscriber::registry()
.with(env_filter)
.with(plain_layer)
.with(json_layer)
.with(console_layer);
match set_global_default(subscriber) {
Ok(_) => tracing::debug!("Tracing subscriber initialized"),
Err(_) => {
tracing::debug!("Tracing subscriber already initialized, skipping");
}
}
Ok(logging.logs_dir.clone())
}
pub fn write_session_log<T: Serialize>(
logs_dir: &Path,
session_id: &str,
session: &T,
) -> Result<PathBuf> {
fs::create_dir_all(logs_dir)
.with_context(|| format!("Failed to create logs directory: {}", logs_dir.display()))?;
let file_path = logs_dir.join(format!("session-{}.json", session_id));
let content = serde_json::to_vec_pretty(session).context("Failed to serialize session log")?;
fs::write(&file_path, content)
.with_context(|| format!("Failed to write session log: {}", file_path.display()))?;
Ok(file_path)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tempfile::TempDir;
#[test]
fn test_write_session_log() {
let dir = TempDir::new().unwrap();
let session = json!({
"session_id": "test-123",
"messages": [
{"role": "user", "content": "hello"}
]
});
let result = write_session_log(dir.path(), "test-123", &session);
assert!(result.is_ok());
let log_path = result.unwrap();
assert!(log_path.exists());
let content = fs::read_to_string(log_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(parsed["session_id"], "test-123");
}
}