lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

init.rs (6741B)


      1 use std::fs;
      2 use std::sync::OnceLock;
      3 
      4 use tracing::info;
      5 use tracing_appender::rolling::{RollingFileAppender, Rotation};
      6 use tracing_subscriber::prelude::*;
      7 use tracing_subscriber::{EnvFilter, fmt};
      8 
      9 use crate::Result;
     10 use crate::options::{LogFileLayout, LoggingOptions};
     11 
     12 static GUARD: OnceLock<tracing_appender::non_blocking::WorkerGuard> = OnceLock::new();
     13 static INIT: OnceLock<()> = OnceLock::new();
     14 
     15 pub fn init_logging(opts: LoggingOptions) -> Result<()> {
     16     if INIT.get().is_some() {
     17         return Ok(());
     18     }
     19 
     20     let writer = if let Some(dir) = &opts.dir {
     21         fs::create_dir_all(dir)?;
     22         let file_appender = build_file_appender(dir, &opts)?;
     23         let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
     24         let _ = GUARD.set(guard);
     25         Some(non_blocking)
     26     } else {
     27         None
     28     };
     29 
     30     let env = resolve_env_filter(opts.default_level.as_deref());
     31     let fmt_layer_file = writer.as_ref().map(|w| {
     32         fmt::layer()
     33             .with_writer(w.clone())
     34             .with_ansi(false)
     35             .with_target(false)
     36     });
     37     let fmt_layer_stdout = if opts.also_stdout() {
     38         Some(fmt::layer().with_writer(std::io::stdout).with_target(false))
     39     } else {
     40         None
     41     };
     42 
     43     let subscriber = tracing_subscriber::registry()
     44         .with(env)
     45         .with(fmt_layer_file)
     46         .with(fmt_layer_stdout);
     47 
     48     subscriber.try_init()?;
     49     let _ = INIT.set(());
     50     info!(
     51         "logging initialized (file: {}, stdout: {})",
     52         opts.resolved_current_log_file_path()
     53             .map(|path| path.display().to_string())
     54             .unwrap_or_else(|| "<disabled>".into()),
     55         opts.also_stdout()
     56     );
     57     Ok(())
     58 }
     59 
     60 fn resolve_env_filter(default_level: Option<&str>) -> EnvFilter {
     61     match default_level {
     62         Some(level) => EnvFilter::new(level),
     63         None => EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
     64     }
     65 }
     66 
     67 pub fn init_stdout() -> Result<()> {
     68     init_logging(LoggingOptions {
     69         dir: None,
     70         file_name: "radroots.log".into(),
     71         stdout: true,
     72         default_level: None,
     73         file_layout: LogFileLayout::PrefixedDate,
     74     })
     75 }
     76 
     77 fn build_file_appender(
     78     dir: &std::path::Path,
     79     opts: &LoggingOptions,
     80 ) -> Result<RollingFileAppender> {
     81     let builder = RollingFileAppender::builder().rotation(Rotation::DAILY);
     82     let builder = match opts.file_layout {
     83         LogFileLayout::PrefixedDate => builder.filename_prefix(opts.file_name.as_str()),
     84         LogFileLayout::DatedFileName => builder.filename_suffix(opts.file_name.as_str()),
     85     };
     86     Ok(builder.build(dir)?)
     87 }
     88 
     89 #[cfg(test)]
     90 mod tests {
     91     use super::{build_file_appender, init_logging, resolve_env_filter};
     92     use crate::{LogFileLayout, LoggingOptions};
     93     use std::path::PathBuf;
     94     use std::time::{SystemTime, UNIX_EPOCH};
     95     use tracing_subscriber::fmt::MakeWriter;
     96 
     97     fn temp_log_dir(name: &str) -> PathBuf {
     98         let nanos = SystemTime::now()
     99             .duration_since(UNIX_EPOCH)
    100             .expect("system time")
    101             .as_nanos();
    102         let dir = std::env::temp_dir().join(format!("radroots_log-{name}-{nanos}"));
    103         let _ = std::fs::remove_dir_all(&dir);
    104         dir
    105     }
    106 
    107     #[test]
    108     fn prefixed_date_layout_keeps_existing_filename_shape() {
    109         let dir = temp_log_dir("prefixed-date");
    110         let appender = build_file_appender(
    111             dir.as_path(),
    112             &LoggingOptions {
    113                 dir: Some(dir.clone()),
    114                 file_name: "myc.log".to_owned(),
    115                 stdout: false,
    116                 default_level: Some("info".to_owned()),
    117                 file_layout: LogFileLayout::PrefixedDate,
    118             },
    119         )
    120         .expect("appender");
    121 
    122         let writer = appender.make_writer();
    123         drop(writer);
    124 
    125         let names: Vec<String> = std::fs::read_dir(&dir)
    126             .expect("read dir")
    127             .map(|entry| {
    128                 entry
    129                     .expect("entry")
    130                     .file_name()
    131                     .to_string_lossy()
    132                     .to_string()
    133             })
    134             .collect();
    135         assert_eq!(names.len(), 1);
    136         assert!(names[0].starts_with("myc.log."));
    137         let _ = std::fs::remove_dir_all(&dir);
    138     }
    139 
    140     #[test]
    141     fn dated_file_name_layout_writes_date_named_log_files() {
    142         let dir = temp_log_dir("dated-file-name");
    143         let appender = build_file_appender(
    144             dir.as_path(),
    145             &LoggingOptions {
    146                 dir: Some(dir.clone()),
    147                 file_name: "log".to_owned(),
    148                 stdout: false,
    149                 default_level: Some("info".to_owned()),
    150                 file_layout: LogFileLayout::DatedFileName,
    151             },
    152         )
    153         .expect("appender");
    154 
    155         let writer = appender.make_writer();
    156         drop(writer);
    157 
    158         let names: Vec<String> = std::fs::read_dir(&dir)
    159             .expect("read dir")
    160             .map(|entry| {
    161                 entry
    162                     .expect("entry")
    163                     .file_name()
    164                     .to_string_lossy()
    165                     .to_string()
    166             })
    167             .collect();
    168         assert_eq!(names.len(), 1);
    169         assert!(names[0].ends_with(".log"));
    170         assert_eq!(names[0].matches('.').count(), 1);
    171         let _ = std::fs::remove_dir_all(&dir);
    172     }
    173 
    174     #[test]
    175     fn init_paths_cover_layout_options() {
    176         let dir = temp_log_dir("init-paths");
    177         std::fs::create_dir_all(&dir).expect("create dir");
    178         let invalid = dir.join("not-a-dir");
    179         std::fs::write(&invalid, "file").expect("write invalid path");
    180         let err_path = init_logging(LoggingOptions {
    181             dir: Some(invalid),
    182             file_name: "x.log".to_string(),
    183             stdout: false,
    184             default_level: None,
    185             file_layout: LogFileLayout::PrefixedDate,
    186         });
    187         assert!(err_path.is_err());
    188 
    189         let first = build_file_appender(
    190             &dir,
    191             &LoggingOptions {
    192                 dir: Some(dir.clone()),
    193                 file_name: "service".to_string(),
    194                 stdout: false,
    195                 default_level: Some("info".to_string()),
    196                 file_layout: LogFileLayout::DatedFileName,
    197             },
    198         );
    199         assert!(first.is_ok());
    200         let _ = std::fs::remove_dir_all(&dir);
    201     }
    202 
    203     #[test]
    204     fn explicit_default_level_wins_over_ambient_rust_log() {
    205         // Callers that pass an explicit service filter should not inherit the shell's RUST_LOG.
    206         let env = resolve_env_filter(Some("info,myc=info"));
    207         let rendered = env.to_string();
    208         assert!(rendered.contains("info"));
    209         assert!(rendered.contains("myc=info"));
    210     }
    211 }