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}