winbrew_cli\services\bootstrap/
logging.rs

1//! One-time tracing and log-sink initialization for the CLI process.
2//!
3//! The CLI initializes logging before database startup and command dispatch so
4//! any later failure, including bootstrap cleanup failures, can be written to
5//! both the terminal and the rotating log file. This module intentionally keeps
6//! the global tracing subscriber setup isolated from the rest of startup.
7
8use anyhow::{Context, Result};
9use std::path::Path;
10use std::sync::OnceLock;
11use tracing_subscriber::{EnvFilter, fmt, prelude::*};
12
13static LOG_GUARD: OnceLock<tracing_appender::non_blocking::WorkerGuard> = OnceLock::new();
14static LOG_INIT: OnceLock<()> = OnceLock::new();
15
16/// Initialize the process-wide tracing subscriber and log file sink.
17/// The function is idempotent: the first successful call installs the global
18/// subscriber, creates the log directory if needed, and keeps the file writer
19/// guard alive for the remainder of the process. Subsequent calls are no-ops.
20/// `log_level` controls the console filter, while `file_log_level` controls the
21/// file sink. Both are parsed through `EnvFilter`, so the configuration accepts
22/// standard tracing filter syntax rather than a bespoke log-level enum.
23pub fn init(log_dir: &Path, log_level: &str, file_log_level: &str) -> Result<()> {
24    if LOG_INIT.get().is_some() {
25        return Ok(());
26    }
27
28    std::fs::create_dir_all(log_dir)
29        .with_context(|| format!("failed to create log directory at {:?}", log_dir))?;
30
31    let file_appender = tracing_appender::rolling::daily(log_dir, "winbrew.log");
32    let (file_writer, guard) = tracing_appender::non_blocking(file_appender);
33    let _ = LOG_GUARD.set(guard);
34
35    let console_filter = EnvFilter::try_new(log_level).context("invalid core.log_level")?;
36    let file_filter = EnvFilter::try_new(file_log_level).context("invalid core.file_log_level")?;
37
38    let console_layer = fmt::layer()
39        .with_target(false)
40        .without_time()
41        .with_filter(console_filter);
42
43    let file_layer = fmt::layer()
44        .with_target(true)
45        .with_thread_ids(true)
46        .with_ansi(false)
47        .with_writer(file_writer)
48        .with_filter(file_filter);
49
50    tracing_subscriber::registry()
51        .with(console_layer)
52        .with(file_layer)
53        .try_init()
54        .context("failed to initialize tracing subscriber")?;
55
56    let _ = LOG_INIT.set(());
57
58    Ok(())
59}