nih_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::Logger;
10use crate::target::OutputTargetImpl;
11use crate::LOGGER_INSTANCE;
12
13/// Constructs an NIH-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 `NIH_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 // The time crate prevents us from getting the local time offset on Linux because other
110 // threads may modify the environment. When this logger is being initialized that should not
111 // be the case.
112 unsafe {
113 time::util::local_offset::set_soundness(time::util::local_offset::Soundness::Unsound)
114 };
115 let local_time_offset = time::UtcOffset::current_local_offset().unwrap_or_else(|_| {
116 eprintln!("Could not get the local time offset, defaulting to UTC");
117 time::UtcOffset::UTC
118 });
119 unsafe {
120 time::util::local_offset::set_soundness(time::util::local_offset::Soundness::Sound)
121 };
122
123 let max_log_level = self.max_log_level;
124 let always_show_module_path = self.always_show_module_path;
125 let logger = Logger {
126 max_log_level,
127 always_show_module_path,
128 // Picking an output target happens in three steps:
129 // - If `LoggerBuilder::with_output_target()` was called, that target is used.
130 // - If the `NIH_LOG` environment variable is non-empty, then that is parsed.
131 // - Otherwise a dynamic target is used that writes to either STDERR or a WinDbg
132 // debugger depending on whether a Windows debugger is present.
133 output_target: Mutex::new(
134 self.output_target
135 .unwrap_or_else(OutputTargetImpl::default_from_environment),
136 ),
137 local_time_offset,
138
139 module_blacklist: self.module_blacklist,
140 };
141
142 // We store a global logger instance and then set a static reference to that as the global
143 // logger. This way we can access the global logger instance later if it needs to be
144 // reconfigured at runtime
145 match LOGGER_INSTANCE.try_insert(logger) {
146 Ok(logger_instance) => {
147 log::set_logger(logger_instance).map_err(|_| SetLoggerError(()))?;
148 log::set_max_level(max_log_level);
149 Ok(())
150 }
151 Err(_) => Err(SetLoggerError(())),
152 }
153 }
154
155 /// Always show the module path. Normally this is only shown for the messages on the `Debug`
156 /// level or on higher verbosity levels. Useful for debugging.
157 pub fn always_show_module_path(mut self) -> Self {
158 self.always_show_module_path = true;
159 self
160 }
161
162 /// Filter out log messages produced by the given crate.
163 pub fn filter_crate(mut self, crate_name: impl Into<String>) -> Self {
164 self.module_blacklist.insert(crate_name.into());
165 self
166 }
167
168 /// Filter out log messages produced by the given module. Module names are matched exactly and
169 /// case sensitively. Filtering based on a module prefix is currently not supported.
170 pub fn filter_module(mut self, crate_name: impl Into<String>) -> Self {
171 // Right now both of these functions do the same thing, in the future we may want to
172 // differentiate between them
173 self.module_blacklist.insert(crate_name.into());
174 self
175 }
176
177 /// Explicitly set the output target for the logger. This is normally set using the `NIH_LOG`
178 /// environment variable. Returns an error if the target could not be set.
179 #[allow(clippy::result_large_err)]
180 pub fn with_output_target(mut self, target: OutputTarget) -> Result<Self, SetTargetError> {
181 self.output_target = Some(match target {
182 OutputTarget::Stderr => OutputTargetImpl::new_stderr(),
183 #[cfg(windows)]
184 OutputTarget::WinDbg => OutputTargetImpl::new_windbg(),
185 OutputTarget::File(path) => match OutputTargetImpl::new_file_path(&path) {
186 Ok(target) => target,
187 Err(error) => {
188 return Err(SetTargetError::FileOpenError {
189 builder: self,
190 path,
191 error,
192 })
193 }
194 },
195 });
196
197 Ok(self)
198 }
199}