Skip to main content

nice_log/
builder.rs

1//! A builder interface for the logger.
2use log::LevelFilter;
3use std::collections::HashSet;
4use std::error::Error;
5use std::fmt::Display;
6use std::path::PathBuf;
7use std::sync::Mutex;
8
9use crate::LOGGER_INSTANCE;
10use crate::logger::Logger;
11use crate::platform::OutputTargetImpl;
12
13/// Constructs an nice-log logger.
14#[derive(Debug)]
15pub struct LoggerBuilder {
16    /// The maximum log level. Set when constructing the builder.
17    max_log_level: LevelFilter,
18    /// If set to `true`, then the module path is always shown. Useful for debug builds and to
19    /// configure the module blacklist.
20    always_show_module_path: bool,
21    /// An explicitly set output target. If this is not set then the target is chosen based on the
22    /// presence and contents of the `NICE_LOG` environment variable.
23    output_target: Option<OutputTargetImpl>,
24    /// Names of crates module paths that should be excluded from the log. Case sensitive, and only
25    /// matches whole crate names and paths. Both the crate name and module path are checked
26    /// separately to allow for a little bit of flexibility.
27    module_blacklist: HashSet<String>,
28}
29
30/// Determines where the logger should write its output. If no explicit target is chosen, then a
31/// default dynamic target is used instead. Check the readme for more information.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum OutputTarget {
34    /// Write directly to STDERR.
35    Stderr,
36    /// Output to the Windows debugger using `OutputDebugString()`.
37    #[cfg(windows)]
38    WinDbg,
39    /// Write the log output to a file.
40    File(PathBuf),
41    // TODO: Functions
42}
43
44/// An error raised when setting the logger's output target. This can be converted back to the
45/// builder using `Into<Builder>`.
46#[derive(Debug)]
47pub enum SetTargetError {
48    FileOpenError {
49        builder: LoggerBuilder,
50        path: PathBuf,
51        error: std::io::Error,
52    },
53}
54
55impl From<SetTargetError> for LoggerBuilder {
56    fn from(value: SetTargetError) -> Self {
57        match value {
58            SetTargetError::FileOpenError { builder, .. } => builder,
59        }
60    }
61}
62
63impl Error for SetTargetError {}
64
65impl Display for SetTargetError {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            SetTargetError::FileOpenError {
69                builder: _,
70                path,
71                error,
72            } => {
73                write!(f, "Could not open '{}' ({})", path.display(), error)
74            }
75        }
76    }
77}
78
79/// An error raised when setting a logger after one has already been set.
80// This is the same as `log::SetLoggerError`, except that we can create one ourselves.
81#[derive(Debug)]
82pub struct SetLoggerError(());
83
84impl Error for SetLoggerError {}
85
86impl Display for SetLoggerError {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        write!(
89            f,
90            "Tried to set a global logger after one has already been configured"
91        )
92    }
93}
94
95impl LoggerBuilder {
96    /// Create a builder for a logger. The logger can be installed using the
97    /// [`build_global()`][Self::build_global()] function.
98    pub fn new(max_log_level: LevelFilter) -> Self {
99        Self {
100            max_log_level,
101            always_show_module_path: false,
102            output_target: None,
103            module_blacklist: HashSet::new(),
104        }
105    }
106
107    /// Install the configured logger as the global logger. The global logger can only be set once.
108    pub fn build_global(self) -> Result<(), SetLoggerError> {
109        /*
110        // The time crate prevents us from getting the local time offset on Linux because other
111        // threads may modify the environment. When this logger is being initialized that should not
112        // be the case.
113        unsafe {
114            time::util::local_offset::set_soundness(time::util::local_offset::Soundness::Unsound)
115        };
116        */
117        let local_time_offset = time::UtcOffset::current_local_offset().unwrap_or_else(|_| {
118            eprintln!("Could not get the local time offset, defaulting to UTC");
119            time::UtcOffset::UTC
120        });
121        /*
122        unsafe {
123            time::util::local_offset::set_soundness(time::util::local_offset::Soundness::Sound)
124        };
125        */
126
127        let max_log_level = self.max_log_level;
128        let always_show_module_path = self.always_show_module_path;
129        let logger = Logger {
130            max_log_level,
131            always_show_module_path,
132            // Picking an output target happens in three steps:
133            // - If `LoggerBuilder::with_output_target()` was called, that target is used.
134            // - If the `NICE_LOG` environment variable is non-empty, then that is parsed.
135            // - Otherwise a dynamic target is used that writes to either STDERR or a WinDbg
136            //   debugger depending on whether a Windows debugger is present.
137            output_target: Mutex::new(
138                self.output_target
139                    .unwrap_or_else(OutputTargetImpl::default_from_environment),
140            ),
141            local_time_offset,
142
143            module_blacklist: self.module_blacklist,
144        };
145
146        // We store a global logger instance and then set a static reference to that as the global
147        // logger. This way we can access the global logger instance later if it needs to be
148        // reconfigured at runtime
149        match LOGGER_INSTANCE.try_insert(logger) {
150            Ok(logger_instance) => {
151                log::set_logger(logger_instance).map_err(|_| SetLoggerError(()))?;
152                log::set_max_level(max_log_level);
153                Ok(())
154            }
155            Err(_) => Err(SetLoggerError(())),
156        }
157    }
158
159    /// Always show the module path. Normally this is only shown for the messages on the `Debug`
160    /// level or on higher verbosity levels. Useful for debugging.
161    pub fn always_show_module_path(mut self) -> Self {
162        self.always_show_module_path = true;
163        self
164    }
165
166    /// Filter out log messages produced by the given crate.
167    pub fn filter_crate(mut self, crate_name: impl Into<String>) -> Self {
168        self.module_blacklist.insert(crate_name.into());
169        self
170    }
171
172    /// Filter out log messages produced by the given module. Module names are matched exactly and
173    /// case sensitively. Filtering based on a module prefix is currently not supported.
174    pub fn filter_module(mut self, crate_name: impl Into<String>) -> Self {
175        // Right now both of these functions do the same thing, in the future we may want to
176        // differentiate between them
177        self.module_blacklist.insert(crate_name.into());
178        self
179    }
180
181    /// Explicitly set the output target for the logger. This is normally set using the `NICE_LOG`
182    /// environment variable. Returns an error if the target could not be set.
183    #[allow(clippy::result_large_err)]
184    pub fn with_output_target(mut self, target: OutputTarget) -> Result<Self, SetTargetError> {
185        self.output_target = Some(match target {
186            OutputTarget::Stderr => OutputTargetImpl::new_stderr(),
187            #[cfg(windows)]
188            OutputTarget::WinDbg => OutputTargetImpl::new_windbg(),
189            OutputTarget::File(path) => match OutputTargetImpl::new_file_path(&path) {
190                Ok(target) => target,
191                Err(error) => {
192                    return Err(SetTargetError::FileOpenError {
193                        builder: self,
194                        path,
195                        error,
196                    });
197                }
198            },
199        });
200
201        Ok(self)
202    }
203}