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}