// REGION: API // api: logging::LogOptions { level: String, to_file: bool } // api: logging::LogOptions::from_cli(cli: &crate::cli::Cli) -> Self // api: logging::init_logging(opts: &LogOptions) -> crate::Result<()> // REGION: API-END // // REGION: RESPONSIBILITIES // - Provide structured logging initialization via tracing, defaulting to stderr. // - Optionally enable file logging at /run/zosstorage/zosstorage.log. // Non-goals: runtime log level reconfiguration or external log forwarders. // REGION: RESPONSIBILITIES-END // // REGION: EXTENSION_POINTS // ext: add env-filter support for selective module verbosity (feature-gated). // ext: add JSON log formatting for machine-readability (feature-gated). // REGION: EXTENSION_POINTS-END // // REGION: SAFETY // safety: initialization must be idempotent; calling twice should not double-install layers. // REGION: SAFETY-END // // REGION: ERROR_MAPPING // errmap: IO errors when opening log file -> crate::Error::Other(anyhow) // REGION: ERROR_MAPPING-END // // REGION: TODO // todo: implement file layer initialization and idempotent guard. // REGION: TODO-END //! Logging initialization and options for zosstorage. //! //! Provides structured logging via the `tracing` ecosystem. Defaults to stderr, //! with an optional file target at /run/zosstorage/zosstorage.log. use crate::Result; use std::fs::OpenOptions; use std::io::{self}; use std::sync::OnceLock; use tracing::Level; use tracing_subscriber::fmt; use tracing_subscriber::prelude::*; use tracing_subscriber::registry::Registry; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::util::SubscriberInitExt; /// Logging options resolved from CLI and/or config. #[derive(Debug, Clone)] pub struct LogOptions { /// Level: "error" | "warn" | "info" | "debug" pub level: String, /// When true, also log to /run/zosstorage/zosstorage.log pub to_file: bool, } impl LogOptions { /// Construct options from [struct Cli](src/cli/mod.rs:1). pub fn from_cli(cli: &crate::cli::Cli) -> Self { Self { level: cli.log_level.to_string(), to_file: cli.log_to_file, } } } fn level_from_str(s: &str) -> Level { match s { "error" => Level::ERROR, "warn" => Level::WARN, "info" => Level::INFO, "debug" => Level::DEBUG, _ => Level::INFO, } } static INIT_GUARD: OnceLock<()> = OnceLock::new(); /// Initialize tracing subscriber according to options. /// Must be idempotent when called once in process lifetime. pub fn init_logging(opts: &LogOptions) -> Result<()> { if INIT_GUARD.get().is_some() { return Ok(()); } let lvl = level_from_str(&opts.level); let stderr_layer = fmt::layer() .with_writer(io::stderr) // no timestamps by default for initramfs .with_ansi(false) .with_level(true) .with_target(false) .with_thread_ids(false) .with_thread_names(false) .with_file(false) .with_line_number(false) .with_filter(LevelFilter::from_level(lvl)); if opts.to_file { let log_path = "/run/zosstorage/zosstorage.log"; if let Ok(file) = OpenOptions::new() .create(true) .write(true) .append(true) .open(log_path) { // Make a writer that clones the file handle per write to satisfy MakeWriter. let make_file = move || file.try_clone().expect("failed to clone log file handle"); let file_layer = fmt::layer() .with_writer(make_file) .with_ansi(false) .with_level(true) .with_target(false) .with_thread_ids(false) .with_thread_names(false) .with_file(false) .with_line_number(false) .with_filter(LevelFilter::from_level(lvl)); Registry::default() .with(stderr_layer) .with(file_layer) .try_init() .map_err(|e| crate::Error::Other(anyhow::anyhow!("failed to set global logger: {}", e)))?; } else { // Fall back to stderr-only if file cannot be opened Registry::default() .with(stderr_layer) .try_init() .map_err(|e| crate::Error::Other(anyhow::anyhow!("failed to set global logger: {}", e)))?; } } else { Registry::default() .with(stderr_layer) .try_init() .map_err(|e| crate::Error::Other(anyhow::anyhow!("failed to set global logger: {}", e)))?; } let _ = INIT_GUARD.set(()); Ok(()) }