|
| 1 | +use eyre::{Result, WrapErr}; |
| 2 | +use std::ffi::OsStr; |
| 3 | +use std::fs::{self, File, OpenOptions}; |
| 4 | +use std::io::{self, IsTerminal, Write}; |
| 5 | +use std::path::{Path, PathBuf}; |
| 6 | +use std::sync::{Arc, Mutex, OnceLock}; |
| 7 | +use tracing_subscriber::fmt; |
| 8 | +use tracing_subscriber::prelude::*; |
| 9 | +use tracing_subscriber::{EnvFilter, registry}; |
| 10 | + |
| 11 | +static LOG_PATH: OnceLock<PathBuf> = OnceLock::new(); |
| 12 | + |
| 13 | +#[derive(Clone)] |
| 14 | +struct SharedFileWriter { |
| 15 | + file: Arc<Mutex<File>>, |
| 16 | +} |
| 17 | + |
| 18 | +impl Write for SharedFileWriter { |
| 19 | + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
| 20 | + let mut file = self |
| 21 | + .file |
| 22 | + .lock() |
| 23 | + .map_err(|_| io::Error::other("failed to lock portal log file"))?; |
| 24 | + file.write(buf) |
| 25 | + } |
| 26 | + |
| 27 | + fn flush(&mut self) -> io::Result<()> { |
| 28 | + let mut file = self |
| 29 | + .file |
| 30 | + .lock() |
| 31 | + .map_err(|_| io::Error::other("failed to lock portal log file"))?; |
| 32 | + file.flush() |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +fn log_dir() -> PathBuf { |
| 37 | + if let Some(state_home) = std::env::var_os("XDG_STATE_HOME") { |
| 38 | + return PathBuf::from(state_home).join("agent-box").join("logs"); |
| 39 | + } |
| 40 | + |
| 41 | + if let Some(home) = std::env::var_os("HOME") { |
| 42 | + return PathBuf::from(home) |
| 43 | + .join(".local") |
| 44 | + .join("state") |
| 45 | + .join("agent-box") |
| 46 | + .join("logs"); |
| 47 | + } |
| 48 | + |
| 49 | + std::env::temp_dir().join("agent-box").join("logs") |
| 50 | +} |
| 51 | + |
| 52 | +pub fn default_log_path(socket_path: Option<&Path>) -> PathBuf { |
| 53 | + let file_name = socket_path |
| 54 | + .and_then(Path::file_name) |
| 55 | + .unwrap_or_else(|| OsStr::new("agent-portal-host.sock")); |
| 56 | + let mut log_name = PathBuf::from(file_name); |
| 57 | + log_name.set_extension("log"); |
| 58 | + log_dir().join(log_name) |
| 59 | +} |
| 60 | + |
| 61 | +pub fn init(log_level: Option<&str>, socket_path: Option<&Path>) -> Result<PathBuf> { |
| 62 | + if let Some(path) = LOG_PATH.get() { |
| 63 | + return Ok(path.clone()); |
| 64 | + } |
| 65 | + |
| 66 | + let log_path = default_log_path(socket_path); |
| 67 | + if let Some(parent) = log_path.parent() { |
| 68 | + fs::create_dir_all(parent).wrap_err("failed to create portal log directory")?; |
| 69 | + } |
| 70 | + |
| 71 | + let file = OpenOptions::new() |
| 72 | + .create(true) |
| 73 | + .append(true) |
| 74 | + .open(&log_path) |
| 75 | + .wrap_err_with(|| format!("failed to open portal log file {}", log_path.display()))?; |
| 76 | + let file = Arc::new(Mutex::new(file)); |
| 77 | + |
| 78 | + let env_filter = match log_level { |
| 79 | + Some(level) => EnvFilter::new(level), |
| 80 | + None => EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), |
| 81 | + }; |
| 82 | + |
| 83 | + let stderr_is_terminal = std::io::stderr().is_terminal(); |
| 84 | + let stderr_layer = fmt::layer() |
| 85 | + .with_ansi(stderr_is_terminal) |
| 86 | + .with_writer(std::io::stderr); |
| 87 | + let file_layer = fmt::layer() |
| 88 | + .with_ansi(false) |
| 89 | + .with_writer(move || SharedFileWriter { |
| 90 | + file: Arc::clone(&file), |
| 91 | + }); |
| 92 | + |
| 93 | + registry() |
| 94 | + .with(env_filter) |
| 95 | + .with(stderr_layer) |
| 96 | + .with(file_layer) |
| 97 | + .try_init() |
| 98 | + .map_err(|e| eyre::eyre!("failed to initialize portal logging: {e}"))?; |
| 99 | + |
| 100 | + let _ = LOG_PATH.set(log_path.clone()); |
| 101 | + tracing::info!(log_file = %log_path.display(), "portal logging initialized"); |
| 102 | + |
| 103 | + Ok(log_path) |
| 104 | +} |
0 commit comments