bom_buddy/
logging.rs

1use crate::config::Config;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use strum_macros::{Display, EnumString};
5use tracing_appender::non_blocking::WorkerGuard;
6use tracing_subscriber::filter::{filter_fn, LevelFilter};
7use tracing_subscriber::fmt;
8use tracing_subscriber::prelude::*;
9
10#[derive(clap::ValueEnum, Copy, Clone, Debug, Serialize, Deserialize, EnumString, Display)]
11#[strum(serialize_all = "snake_case")]
12#[serde(rename_all = "snake_case")]
13pub enum LogLevel {
14    Off,
15    Error,
16    Warn,
17    Info,
18    Debug,
19    Trace,
20}
21
22impl From<LogLevel> for LevelFilter {
23    fn from(level: LogLevel) -> Self {
24        match level {
25            LogLevel::Off => LevelFilter::OFF,
26            LogLevel::Error => LevelFilter::ERROR,
27            LogLevel::Warn => LevelFilter::WARN,
28            LogLevel::Info => LevelFilter::INFO,
29            LogLevel::Debug => LevelFilter::DEBUG,
30            LogLevel::Trace => LevelFilter::TRACE,
31        }
32    }
33}
34
35#[derive(Debug, Deserialize, Serialize)]
36pub struct LoggingOptions {
37    pub use_stderr: bool,
38    pub console_level: LogLevel,
39    pub file_path: PathBuf,
40    pub file_level: LogLevel,
41    pub journal_level: LogLevel,
42    pub rotate_logs: bool,
43    pub exclude_external: bool,
44}
45
46impl Default for LoggingOptions {
47    fn default() -> Self {
48        Self {
49            use_stderr: true,
50            console_level: LogLevel::Info,
51            file_path: Config::default_dirs().run.join("bom-buddy.log"),
52            file_level: LogLevel::Debug,
53            journal_level: LogLevel::Off,
54            rotate_logs: false,
55            exclude_external: true,
56        }
57    }
58}
59
60#[derive(Default)]
61pub struct LogGuards {
62    file: Option<WorkerGuard>,
63    console: Option<WorkerGuard>,
64}
65
66pub fn setup_logging(opts: &LoggingOptions) -> LogGuards {
67    let mut layers = Vec::new();
68    let mut guards = LogGuards::default();
69    let exclude_external = if opts.exclude_external {
70        Some(filter_fn(|metadata| {
71            metadata.target().starts_with("bom_buddy")
72        }))
73    } else {
74        None
75    };
76
77    let console_level: LevelFilter = opts.console_level.into();
78    if console_level > LevelFilter::OFF {
79        let (console_writer, _guard) = if opts.use_stderr {
80            tracing_appender::non_blocking(std::io::stderr())
81        } else {
82            tracing_appender::non_blocking(std::io::stdout())
83        };
84        guards.console = Some(_guard);
85        let console_layer = tracing_subscriber::fmt::layer()
86            .with_writer(console_writer)
87            .with_filter::<LevelFilter>(opts.console_level.into())
88            .with_filter(exclude_external.clone())
89            .boxed();
90        layers.push(console_layer);
91    }
92
93    let file_level: LevelFilter = opts.file_level.into();
94    if file_level > LevelFilter::OFF {
95        let log_dir = opts.file_path.parent().unwrap();
96        let file_name = opts.file_path.file_name().unwrap();
97        let file_appender = if opts.rotate_logs {
98            tracing_appender::rolling::daily(log_dir, file_name)
99        } else {
100            tracing_appender::rolling::never(log_dir, file_name)
101        };
102        let (file_writer, guard) = tracing_appender::non_blocking(file_appender);
103        guards.file = Some(guard);
104        let file_layer = fmt::Layer::default()
105            .with_writer(file_writer)
106            .with_ansi(false)
107            .with_filter::<LevelFilter>(opts.file_level.into())
108            .with_filter(exclude_external.clone())
109            .boxed();
110        layers.push(file_layer);
111    }
112
113    let journal_level: LevelFilter = opts.journal_level.into();
114    if journal_level > LevelFilter::OFF {
115        let journal_layer = tracing_journald::layer()
116            .expect("Couldn't connect to journald")
117            .with_filter::<LevelFilter>(opts.journal_level.into())
118            .with_filter(exclude_external.clone())
119            .boxed();
120        layers.push(journal_layer);
121    }
122
123    tracing_subscriber::registry().with(layers).init();
124    guards
125}