log_nonblock/
lib.rs

1#[cfg(feature = "colored")]
2use colored::Colorize;
3use log::{LevelFilter, Log, Metadata, Record, SetLoggerError};
4use std::os::fd::AsRawFd;
5use std::sync::Arc;
6use std::sync::atomic::AtomicBool;
7#[cfg(feature = "timestamps")]
8use time::{OffsetDateTime, UtcOffset, format_description::FormatItem};
9
10mod io;
11mod worker;
12
13#[cfg(feature = "timestamps")]
14#[derive(Clone, Debug, PartialEq)]
15enum Timestamps {
16    None,
17    Utc,
18    UtcOffset(UtcOffset),
19}
20
21#[cfg(feature = "timestamps")]
22const TIMESTAMP_FORMAT_OFFSET: &[FormatItem] = time::macros::format_description!(
23    "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3][offset_hour sign:mandatory]:[offset_minute]"
24);
25
26#[cfg(feature = "timestamps")]
27const TIMESTAMP_FORMAT_UTC: &[FormatItem] = time::macros::format_description!(
28    "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z"
29);
30
31#[derive(Clone, Debug)]
32pub struct NonBlockingOptions {
33    /// The default logging level
34    default_level: LevelFilter,
35
36    /// The specific logging level for each module
37    ///
38    /// This is used to override the default value for some specific modules.
39    ///
40    /// This must be sorted from most-specific to least-specific, so that [`enabled`](#method.enabled) can scan the
41    /// vector for the first match to give us the desired log level for a module.
42    module_levels: Vec<(String, LevelFilter)>,
43
44    #[cfg(feature = "colors")]
45    colors: bool,
46
47    #[cfg(feature = "timestamps")]
48    timestamps: Timestamps,
49
50    #[cfg(feature = "timestamps")]
51    timestamps_format: Option<&'static [FormatItem<'static>]>,
52}
53
54pub struct NonBlockingLoggerBuilder {
55    options: NonBlockingOptions,
56}
57
58impl Default for NonBlockingLoggerBuilder {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl NonBlockingLoggerBuilder {
65    pub fn new() -> Self {
66        Self {
67            options: NonBlockingOptions {
68                default_level: LevelFilter::Trace,
69                module_levels: Vec::new(),
70
71                #[cfg(feature = "threads")]
72                threads: false,
73
74                #[cfg(feature = "timestamps")]
75                timestamps: Timestamps::Utc,
76
77                #[cfg(feature = "timestamps")]
78                timestamps_format: None,
79
80                #[cfg(feature = "colors")]
81                colors: true,
82            },
83        }
84    }
85
86    #[must_use = "You must call init() to begin logging"]
87    pub fn with_module_level(mut self, target: &str, level: LevelFilter) -> Self {
88        self.options.module_levels.push((target.to_string(), level));
89        self.options
90            .module_levels
91            .sort_by_key(|(name, _level)| name.len().wrapping_neg());
92        self
93    }
94
95    /// Control whether messages are colored or not.
96    ///
97    /// This method is only available if the `colored` feature is enabled.
98    #[must_use = "You must call init() to begin logging"]
99    #[cfg(feature = "colors")]
100    pub fn with_colors(mut self, colors: bool) -> Self {
101        self.options.colors = colors;
102        self
103    }
104
105    /// Don't display any timestamps.
106    ///
107    /// This method is only available if the `timestamps` feature is enabled.
108    #[must_use = "You must call init() to begin logging"]
109    #[cfg(feature = "timestamps")]
110    pub fn without_timestamps(mut self) -> Self {
111        self.options.timestamps = Timestamps::None;
112        self
113    }
114
115    /// Display timestamps using UTC.
116    ///
117    /// This method is only available if the `timestamps` feature is enabled.
118    #[must_use = "You must call init() to begin logging"]
119    #[cfg(feature = "timestamps")]
120    pub fn with_utc_timestamps(mut self) -> Self {
121        self.options.timestamps = Timestamps::Utc;
122        self
123    }
124
125    /// Display timestamps using a static UTC offset.
126    ///
127    /// This method is only available if the `timestamps` feature is enabled.
128    #[must_use = "You must call init() to begin logging"]
129    #[cfg(feature = "timestamps")]
130    pub fn with_utc_offset(mut self, offset: UtcOffset) -> Self {
131        self.options.timestamps = Timestamps::UtcOffset(offset);
132        self
133    }
134
135    /// Control the format used for timestamps.
136    ///
137    /// Without this, a default format is used depending on the timestamps type.
138    ///
139    /// The syntax for the format_description macro can be found in the
140    /// [`time` crate book](https://time-rs.github.io/book/api/format-description.html).
141    #[must_use = "You must call init() to begin logging"]
142    #[cfg(feature = "timestamps")]
143    pub fn with_timestamp_format(mut self, format: &'static [FormatItem<'static>]) -> Self {
144        self.options.timestamps_format = Some(format);
145        self
146    }
147
148    pub fn init(self) -> Result<NonBlockingLogger, SetLoggerError> {
149        #[cfg(all(feature = "colored", feature = "stderr"))]
150        use_stderr_for_colors();
151
152        #[cfg(not(feature = "stderr"))]
153        {
154            if let Err(err) = io::set_nonblocking(std::io::stdout().as_raw_fd()) {
155                io::write_stderr_with_retry(&format!(
156                    "Failed to set STDOUT to non-blocking mode: {}",
157                    err
158                ));
159            }
160        }
161
162        #[cfg(feature = "stderr")]
163        {
164            if let Err(err) = io::set_nonblocking(std::io::stderr().as_raw_fd()) {
165                io::write_stderr_with_retry(&format!(
166                    "Failed to set STDERR to non-blocking mode: {}",
167                    err
168                ));
169            }
170        }
171
172        let (sender, receiver) = crossbeam_channel::bounded(1024);
173
174        let (worker, running) = worker::LogWorker::new(receiver);
175        if let Err(err) = worker.spawn() {
176            println!("Failed to spawn logger worker: {}", err);
177        };
178
179        let logger = NonBlockingLogger {
180            options: self.options,
181            sender,
182            running,
183        };
184
185        log::set_max_level(logger.max_level());
186        log::set_boxed_logger(Box::new(logger.clone()))?;
187
188        Ok(logger)
189    }
190}
191
192#[derive(Debug)]
193pub enum NonBlockingLoggerError {
194    Error { reason: String },
195}
196
197impl std::fmt::Display for NonBlockingLoggerError {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        match self {
200            NonBlockingLoggerError::Error { reason } => {
201                write!(f, "NonBlockingLoggerError: {}", reason)
202            }
203        }
204    }
205}
206
207impl std::error::Error for NonBlockingLoggerError {}
208
209#[derive(Clone, Debug)]
210pub struct NonBlockingLogger {
211    options: NonBlockingOptions,
212    sender: crossbeam_channel::Sender<worker::WorkerMessage>,
213    running: Arc<AtomicBool>,
214}
215
216impl NonBlockingLogger {
217    pub fn max_level(&self) -> LevelFilter {
218        let max_level = self
219            .options
220            .module_levels
221            .iter()
222            .map(|(_name, level)| level)
223            .copied()
224            .max();
225        max_level
226            .map(|lvl| lvl.max(self.options.default_level))
227            .unwrap_or(self.options.default_level)
228    }
229
230    pub fn shutdown(self) -> Result<(), NonBlockingLoggerError> {
231        let compare = self.running.compare_exchange(
232            true,
233            false,
234            std::sync::atomic::Ordering::SeqCst,
235            std::sync::atomic::Ordering::SeqCst,
236        );
237
238        if compare.is_err() {
239            Err(NonBlockingLoggerError::Error {
240                reason: "Failed to shutdown logger: It was already shutted down".to_string(),
241            })
242        } else {
243            Ok(())
244        }
245    }
246}
247
248impl Log for NonBlockingLogger {
249    fn enabled(&self, metadata: &Metadata) -> bool {
250        &metadata.level().to_level_filter()
251            <= self
252                .options
253                .module_levels
254                .iter()
255                /* At this point the Vec is already sorted so that we can simply take
256                 * the first match
257                 */
258                .find(|(name, _level)| metadata.target().starts_with(name))
259                .map(|(_name, level)| level)
260                .unwrap_or(&self.options.default_level)
261    }
262
263    fn log(&self, record: &Record) {
264        if self.enabled(record.metadata()) {
265            let level_string = {
266                #[cfg(feature = "colors")]
267                {
268                    if self.options.colors {
269                        match record.level() {
270                            log::Level::Error => format!("{:<5}", record.level().to_string())
271                                .red()
272                                .to_string(),
273                            log::Level::Warn => format!("{:<5}", record.level().to_string())
274                                .yellow()
275                                .to_string(),
276                            log::Level::Info => format!("{:<5}", record.level().to_string())
277                                .cyan()
278                                .to_string(),
279                            log::Level::Debug => format!("{:<5}", record.level().to_string())
280                                .purple()
281                                .to_string(),
282                            log::Level::Trace => format!("{:<5}", record.level().to_string())
283                                .normal()
284                                .to_string(),
285                        }
286                    } else {
287                        format!("{:<5}", record.level().to_string())
288                    }
289                }
290                #[cfg(not(feature = "colors"))]
291                {
292                    format!("{:<5}", record.level().to_string())
293                }
294            };
295
296            let target = if !record.target().is_empty() {
297                record.target()
298            } else {
299                record.module_path().unwrap_or_default()
300            };
301
302            let thread = {
303                #[cfg(feature = "threads")]
304                if self.options.threads {
305                    let thread = std::thread::current();
306
307                    format!("@{}", {
308                        #[cfg(feature = "nightly")]
309                        {
310                            thread.name().unwrap_or(&thread.id().as_u64().to_string())
311                        }
312
313                        #[cfg(not(feature = "nightly"))]
314                        {
315                            thread.name().unwrap_or("?")
316                        }
317                    })
318                } else {
319                    "".to_string()
320                }
321
322                #[cfg(not(feature = "threads"))]
323                ""
324            };
325
326            let timestamp = {
327                #[cfg(feature = "timestamps")]
328                match self.options.timestamps {
329                    Timestamps::None => "".to_string(),
330                    Timestamps::Utc => format!(
331                        "{} ",
332                        OffsetDateTime::now_utc()
333                            .format(
334                                &self
335                                    .options
336                                    .timestamps_format
337                                    .unwrap_or(TIMESTAMP_FORMAT_UTC)
338                            )
339                            .unwrap()
340                    ),
341                    Timestamps::UtcOffset(offset) => format!(
342                        "{} ",
343                        OffsetDateTime::now_utc()
344                            .to_offset(offset)
345                            .format(
346                                &self
347                                    .options
348                                    .timestamps_format
349                                    .unwrap_or(TIMESTAMP_FORMAT_OFFSET)
350                            )
351                            .unwrap()
352                    ),
353                }
354
355                #[cfg(not(feature = "timestamps"))]
356                ""
357            };
358
359            let message = format!(
360                "{}{} [{}{}] {}\r\n",
361                timestamp,
362                level_string,
363                target,
364                thread,
365                record.args()
366            );
367
368            if let Err(err) = self.sender.send(worker::WorkerMessage::Log(message)) {
369                io::write_stderr_with_retry(&format!("Failed to schedule log: {}", err));
370            }
371        }
372    }
373
374    fn flush(&self) {
375        let (done_tx, done_rx) = crossbeam_channel::bounded(1);
376
377        match self.sender.send(worker::WorkerMessage::Flush(done_tx)) {
378            Ok(_) => {
379                // Block until flush completes
380                let _ = done_rx.recv();
381            }
382            Err(err) => {
383                io::write_stderr_with_retry(&format!(
384                    "Failed to send flush request to logger worker: {}",
385                    err
386                ));
387            }
388        }
389    }
390}
391
392/// The colored crate will disable colors when STDOUT is not a terminal. This method overrides this
393/// behavior to check the status of STDERR instead.
394#[cfg(all(feature = "colored", feature = "stderr"))]
395fn use_stderr_for_colors() {
396    use std::io::{IsTerminal, stderr};
397
398    colored::control::set_override(stderr().is_terminal());
399}