log_nonblock/
lib.rs

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