use std::sync::OnceLock;
static INIT: OnceLock<()> = OnceLock::new();
static PRODUCER_TX: OnceLock<std::sync::mpsc::SyncSender<crate::logging_event::LogEventV1>> =
OnceLock::new();
fn parse_level() -> tracing::Level {
match std::env::var("ATM_LOG")
.unwrap_or_else(|_| "info".to_string())
.to_ascii_lowercase()
.as_str()
{
"trace" => tracing::Level::TRACE,
"debug" => tracing::Level::DEBUG,
"warn" => tracing::Level::WARN,
"error" => tracing::Level::ERROR,
_ => tracing::Level::INFO,
}
}
#[deprecated(since = "0.17.0", note = "Use init_unified() instead")]
pub fn init() {
_init_stderr();
}
fn _init_stderr() {
if INIT.get().is_some() {
return;
}
let level = parse_level();
let _ = tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_max_level(level)
.with_target(false)
.try_init();
let _ = INIT.set(());
}
#[derive(Debug, Clone)]
pub struct RotationConfig {
pub max_bytes: u64,
pub max_files: u32,
}
impl Default for RotationConfig {
fn default() -> Self {
Self {
max_bytes: 50 * 1024 * 1024,
max_files: 5,
}
}
}
#[derive(Debug, Clone)]
pub enum UnifiedLogMode {
ProducerFanIn {
daemon_socket: std::path::PathBuf,
fallback_spool_dir: std::path::PathBuf,
},
DaemonWriter {
file_path: std::path::PathBuf,
rotation: RotationConfig,
},
StderrOnly,
}
pub struct LoggingGuards {
_guards: Vec<Box<dyn std::any::Any + Send>>,
}
impl LoggingGuards {
fn empty() -> Self {
Self {
_guards: Vec::new(),
}
}
}
pub fn init_unified(
source_binary: &'static str,
mode: UnifiedLogMode,
) -> anyhow::Result<LoggingGuards> {
_init_stderr();
match mode {
UnifiedLogMode::StderrOnly => {
Ok(LoggingGuards::empty())
}
UnifiedLogMode::ProducerFanIn {
daemon_socket,
fallback_spool_dir,
} => setup_producer_fan_in(source_binary, daemon_socket, fallback_spool_dir),
UnifiedLogMode::DaemonWriter {
file_path,
rotation,
} => setup_daemon_writer(file_path, rotation),
}
}
pub fn init_stderr_only() -> LoggingGuards {
_init_stderr();
LoggingGuards::empty()
}
pub fn producer_sender()
-> Option<&'static std::sync::mpsc::SyncSender<crate::logging_event::LogEventV1>> {
PRODUCER_TX.get()
}
fn setup_producer_fan_in(
source_binary: &'static str,
daemon_socket: std::path::PathBuf,
fallback_spool_dir: std::path::PathBuf,
) -> anyhow::Result<LoggingGuards> {
use std::sync::mpsc;
let (tx, rx) = mpsc::sync_channel::<crate::logging_event::LogEventV1>(512);
let _ = PRODUCER_TX.set(tx);
let handle = std::thread::Builder::new()
.name("atm-log-forwarder".to_string())
.spawn(move || {
run_forwarder(source_binary, rx, &daemon_socket, &fallback_spool_dir);
})?;
Ok(LoggingGuards {
_guards: vec![Box::new(ForwarderHandle(Some(handle)))],
})
}
struct ForwarderHandle(Option<std::thread::JoinHandle<()>>);
impl Drop for ForwarderHandle {
fn drop(&mut self) {
let _ = self.0.take(); }
}
fn run_forwarder(
_source_binary: &'static str,
rx: std::sync::mpsc::Receiver<crate::logging_event::LogEventV1>,
daemon_socket: &std::path::Path,
fallback_spool_dir: &std::path::Path,
) {
for event in &rx {
if !try_forward_to_socket(&event, daemon_socket) {
crate::logging_event::write_to_spool_dir(&event, fallback_spool_dir);
}
}
}
#[cfg(unix)]
fn try_forward_to_socket(
event: &crate::logging_event::LogEventV1,
daemon_socket: &std::path::Path,
) -> bool {
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::time::Duration;
let stream = match UnixStream::connect(daemon_socket) {
Ok(s) => s,
Err(_) => return false,
};
let timeout = Duration::from_millis(100);
let _ = stream.set_write_timeout(Some(timeout));
let _ = stream.set_read_timeout(Some(timeout));
let payload = match serde_json::to_value(event) {
Ok(v) => v,
Err(_) => return false,
};
let request = crate::daemon_client::SocketRequest {
version: crate::daemon_client::PROTOCOL_VERSION,
request_id: format!(
"log-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos()
),
command: "log-event".to_string(),
payload,
};
let line = match serde_json::to_string(&request) {
Ok(l) => l,
Err(_) => return false,
};
{
let mut writer = std::io::BufWriter::new(&stream);
if writer.write_all(line.as_bytes()).is_err() {
return false;
}
if writer.write_all(b"\n").is_err() {
return false;
}
if writer.flush().is_err() {
return false;
}
}
let mut reader = BufReader::new(&stream);
let mut _response = String::new();
let _ = reader.read_line(&mut _response);
true
}
#[cfg(not(unix))]
fn try_forward_to_socket(
_event: &crate::logging_event::LogEventV1,
_daemon_socket: &std::path::Path,
) -> bool {
false
}
fn setup_daemon_writer(
file_path: std::path::PathBuf,
rotation: RotationConfig,
) -> anyhow::Result<LoggingGuards> {
if let Some(parent) = file_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let mut guards = Vec::<Box<dyn std::any::Any + Send>>::new();
if let Ok(home_dir) = crate::home::get_home_dir() {
let daemon_socket = home_dir.join(".claude/daemon/atm-daemon.sock");
let fallback_spool_dir = crate::logging_event::spool_dir(&home_dir);
match setup_producer_fan_in("atm-daemon", daemon_socket, fallback_spool_dir) {
Ok(forwarder_guards) => guards.extend(forwarder_guards._guards),
Err(err) => tracing::warn!("DaemonWriter: failed to initialize producer fan-in: {err}"),
}
} else {
tracing::warn!("DaemonWriter: failed to resolve ATM home for producer fan-in setup");
}
tracing::debug!(
path = %file_path.display(),
max_bytes = rotation.max_bytes,
max_files = rotation.max_files,
"DaemonWriter logging initialized"
);
Ok(LoggingGuards { _guards: guards })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_init_stderr_only_returns_guards() {
let _g = init_stderr_only();
}
#[test]
fn test_rotation_config_default() {
let cfg = RotationConfig::default();
assert_eq!(cfg.max_bytes, 50 * 1024 * 1024);
assert_eq!(cfg.max_files, 5);
}
#[test]
fn test_init_unified_stderr_only() {
let result = init_unified("test", UnifiedLogMode::StderrOnly);
assert!(result.is_ok());
}
#[test]
fn test_init_unified_daemon_writer() {
let tmp = tempfile::tempdir().unwrap();
let file_path = tmp.path().join("test.jsonl");
let result = init_unified(
"test-daemon",
UnifiedLogMode::DaemonWriter {
file_path,
rotation: RotationConfig::default(),
},
);
assert!(result.is_ok());
}
}