palo-tui 0.5.5

Terminal user interface for Palo local development orchestration
Documentation
use std::{
    fs::{self, File, OpenOptions},
    io::{self, Write},
    path::{Path, PathBuf},
    time::{SystemTime, UNIX_EPOCH},
};

use tracing::{debug, info};
use tracing_subscriber::{EnvFilter, fmt::MakeWriter};

pub const PALO_LOG_FILE_ENV: &str = "PALO_LOG_FILE";
pub const DEFAULT_LOG_DIRECTORY: &str = ".palo/logs";

#[derive(Clone, Debug)]
struct AppendLogWriter {
    path: PathBuf,
}

#[derive(Debug)]
struct AppendLogFile {
    path: PathBuf,
    file: Option<File>,
}

impl AppendLogWriter {
    fn new(path: PathBuf) -> Self {
        Self { path }
    }
}

impl AppendLogFile {
    fn file(&mut self) -> io::Result<&mut File> {
        if self.file.is_none() {
            self.file = Some(
                OpenOptions::new()
                    .create(true)
                    .append(true)
                    .open(&self.path)?,
            );
        }

        Ok(self
            .file
            .as_mut()
            .expect("log file should be opened before use"))
    }
}

impl<'a> MakeWriter<'a> for AppendLogWriter {
    type Writer = AppendLogFile;

    fn make_writer(&'a self) -> Self::Writer {
        AppendLogFile {
            path: self.path.clone(),
            file: None,
        }
    }
}

impl Write for AppendLogFile {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        match self.file() {
            Ok(file) => file.write(buf),
            Err(_) => Ok(buf.len()),
        }
    }

    fn flush(&mut self) -> io::Result<()> {
        match self.file() {
            Ok(file) => file.flush(),
            Err(_) => Ok(()),
        }
    }
}

pub fn default_workspace_log_path(workspace_root: impl AsRef<Path>) -> PathBuf {
    palo_log_path(default_run_log_directory(workspace_root, SystemTime::now()))
}

pub fn default_workspace_log_directory(workspace_root: impl AsRef<Path>) -> PathBuf {
    workspace_root.as_ref().join(DEFAULT_LOG_DIRECTORY)
}

pub fn default_run_log_directory(
    workspace_root: impl AsRef<Path>,
    started_at: SystemTime,
) -> PathBuf {
    run_log_directory(default_workspace_log_directory(workspace_root), started_at)
}

pub fn run_log_directory(base_directory: impl AsRef<Path>, started_at: SystemTime) -> PathBuf {
    base_directory
        .as_ref()
        .join(format_run_timestamp(started_at))
}

pub fn palo_log_path(run_directory: impl AsRef<Path>) -> PathBuf {
    run_directory.as_ref().join("palo.log")
}

pub fn format_run_timestamp(started_at: SystemTime) -> String {
    let duration = started_at.duration_since(UNIX_EPOCH).unwrap_or_default();
    format!("{}_{:03}", duration.as_secs(), duration.subsec_millis())
}

pub fn init_tui_tracing(default_log_path: Option<PathBuf>) {
    let filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("info,palo=info,palo_core=info,palo_tui=info"));

    if let Some(path) = tui_log_path(default_log_path) {
        if let Some(parent) = path.parent() {
            let _ = fs::create_dir_all(parent);
        }

        let writer = AppendLogWriter::new(path.clone());
        let initialized = tracing_subscriber::fmt()
            .with_env_filter(filter)
            .with_target(false)
            .with_ansi(false)
            .with_writer(writer)
            .try_init()
            .is_ok();

        if initialized {
            info!(
                path = %path.display(),
                "palo tui tracing redirected away from terminal"
            );
        }
    } else {
        let initialized = tracing_subscriber::fmt()
            .with_env_filter(filter)
            .with_target(false)
            .with_ansi(false)
            .with_writer(io::sink)
            .try_init()
            .is_ok();

        if initialized {
            debug!("palo tui tracing discarded because no log file path was available");
        }
    }
}

fn tui_log_path(default_log_path: Option<PathBuf>) -> Option<PathBuf> {
    match std::env::var_os(PALO_LOG_FILE_ENV) {
        Some(value) if !value.is_empty() => Some(PathBuf::from(value)),
        Some(_) => None,
        None => default_log_path,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_run_log_directory_uses_timestamped_palo_directory() {
        let started_at = UNIX_EPOCH + std::time::Duration::from_millis(1_700_000_000_042);

        assert_eq!(
            default_run_log_directory("/workspace", started_at),
            PathBuf::from("/workspace/.palo/logs/1700000000_042")
        );
    }

    #[test]
    fn palo_log_path_lives_inside_run_directory() {
        assert_eq!(
            palo_log_path("/workspace/.palo/logs/1700000000_042"),
            PathBuf::from("/workspace/.palo/logs/1700000000_042/palo.log")
        );
    }
}