async_rawlogger/
lib.rs

1use async_channel::{Receiver, Sender, bounded, unbounded};
2pub use log::{
3    Level, LevelFilter, Record, debug, error, info, log, log_enabled, logger, trace, warn,
4};
5use log::{Log, Metadata, SetLoggerError, set_boxed_logger, set_max_level};
6use logmsg::LogMsg;
7use std::{
8    borrow::Cow,
9    // fmt::Display,
10    io::Error as IoError,
11    sync::atomic::{AtomicBool, Ordering},
12};
13mod logmsg;
14
15enum LoggerInput {
16    LogMsg(LogMsg),
17    Flush,
18}
19
20enum LoggerOutput {
21    Flushed,
22}
23
24/// A guard that flushes logs associated to a Logger on a drop
25///
26/// With this guard, you can ensure all logs are written to destination
27/// when the application exits.
28pub struct LoggerGuard {
29    queue: Sender<LoggerInput>,
30    notification: Receiver<LoggerOutput>,
31}
32
33impl Drop for LoggerGuard {
34    fn drop(&mut self) {
35        let _ = self.queue.send_blocking(LoggerInput::Flush);
36        let _ = self.notification.recv_blocking();
37    }
38}
39
40/// Global logger
41pub struct Logger {
42    level: LevelFilter,
43    queue: Sender<LoggerInput>,
44    notification: Receiver<LoggerOutput>,
45    stopped: AtomicBool,
46}
47
48impl Logger {
49    pub fn init(self) -> Result<LoggerGuard, SetLoggerError> {
50        let guard = LoggerGuard {
51            queue: self.queue.clone(),
52            notification: self.notification.clone(),
53        };
54
55        set_max_level(self.level);
56        let boxed = Box::new(self);
57        set_boxed_logger(boxed).map(|_| guard)
58    }
59}
60
61impl Log for Logger {
62    #[inline]
63    fn enabled(
64        &self,
65        metadata: &Metadata,
66    ) -> bool {
67        self.level >= metadata.level()
68    }
69
70    fn log(
71        &self,
72        record: &Record,
73    ) {
74        let msg = LogMsg {
75            time: std::time::SystemTime::now(),
76            level: record.level(),
77            file: record
78                .file_static()
79                .map(Cow::Borrowed)
80                .or_else(|| record.file().map(|s| Cow::Owned(s.to_owned())))
81                .unwrap_or(Cow::Borrowed("")),
82            line: record.line(),
83            args: record
84                .args()
85                .as_str()
86                .map(Cow::Borrowed)
87                .unwrap_or_else(|| Cow::Owned(record.args().to_string())),
88        };
89
90        match self.queue.try_send(LoggerInput::LogMsg(msg)) {
91            Err(async_channel::TrySendError::Full(_)) => {}
92            Err(async_channel::TrySendError::Closed(_)) => {
93                let stop = self.stopped.load(Ordering::SeqCst);
94                if !stop {
95                    eprintln!("logger queue closed when logging, this is a bug");
96                    self.stopped.store(true, Ordering::SeqCst);
97                }
98            }
99            Ok(_) => {}
100        }
101    }
102
103    fn flush(&self) {
104        let _ = self
105            .queue
106            .send_blocking(LoggerInput::Flush)
107            .map_err(|e| eprintln!("logger queue closed when flushing, this is a bug: {e}"));
108    }
109}
110
111struct BoundedChannelOption {
112    size: usize,
113}
114
115pub struct Builder {
116    level: Option<LevelFilter>,
117    root_level: Option<LevelFilter>,
118    bounded_channel_option: Option<BoundedChannelOption>,
119}
120
121/// Handy function to get ftlog builder
122#[inline]
123pub fn builder() -> Builder {
124    Builder::new()
125}
126
127impl Builder {
128    #[inline]
129    /// Create a ftlog builder with default settings:
130    /// - global log level: INFO
131    /// - root log level: INFO
132    /// - bounded channel between worker thread and log thread, with a size limit of 100_000
133    /// - discard excessive log messages
134    pub fn new() -> Builder {
135        Builder {
136            level: None,
137            root_level: None,
138            bounded_channel_option: Some(BoundedChannelOption { size: 100_000 }),
139        }
140    }
141
142    /// Set channel size to unbound
143    ///
144    /// **ATTENTION**: too much log message will lead to huge memory consumption,
145    /// as log messages are queued to be handled by log thread.
146    /// When log message exceed the current channel size, it will double the size by default,
147    /// Since channel expansion asks for memory allocation, log calls can be slow down.
148    #[inline]
149    pub fn unbounded(mut self) -> Builder {
150        self.bounded_channel_option = None;
151        self
152    }
153
154    #[inline]
155    /// Set max log level
156    ///
157    /// Logs with level more verbose than this will not be sent to log thread.
158    pub fn max_log_level(
159        mut self,
160        level: LevelFilter,
161    ) -> Builder {
162        self.level = Some(level);
163        self
164    }
165
166    #[inline]
167    /// Set max log level
168    ///
169    /// Logs with level more verbose than this will not be sent to log thread.
170    pub fn root_log_level(
171        mut self,
172        level: LevelFilter,
173    ) -> Builder {
174        self.root_level = Some(level);
175        self
176    }
177
178    /// Finish building ftlog logger
179    ///
180    /// The call spawns a log thread to formatting log message into string,
181    /// and write to output target.
182    pub fn build(self) -> Result<Logger, IoError> {
183        let global_level = self.level.unwrap_or(LevelFilter::Info);
184        let root_level = self.root_level.unwrap_or(global_level);
185        if global_level < root_level {
186            warn!("Logs with level more verbose than {global_level} will be ignored");
187        }
188
189        let (queue, rx) = match self.bounded_channel_option {
190            Some(BoundedChannelOption { size }) => bounded(size),
191            None => unbounded(),
192        };
193
194        let (notification_tx, notification) = bounded(1);
195
196        std::thread::spawn(move || {
197            let mut batch = Vec::with_capacity(100);
198            let mut last_flush = std::time::Instant::now();
199            let batch_timeout = std::time::Duration::from_millis(100);
200
201            while let Ok(msg) = rx.recv_blocking() {
202                match msg {
203                    LoggerInput::LogMsg(msg) => {
204                        batch.push(msg);
205                        if batch.len() >= 100 || last_flush.elapsed() > batch_timeout {
206                            for msg in batch.drain(..) {
207                                msg.write();
208                            }
209                            last_flush = std::time::Instant::now();
210                        }
211                    }
212                    LoggerInput::Flush => {
213                        for msg in batch.drain(..) {
214                            msg.write();
215                        }
216                        let _ = notification_tx.send_blocking(LoggerOutput::Flushed);
217                    }
218                }
219            }
220        });
221
222        Ok(Logger {
223            level: global_level,
224            queue,
225            notification,
226            stopped: AtomicBool::new(false),
227        })
228    }
229
230    /// Try building and setting as global logger
231    pub fn try_init(self) -> Result<LoggerGuard, Box<dyn std::error::Error>> {
232        let logger = self.build()?;
233        Ok(logger.init()?)
234    }
235}
236
237impl Default for Builder {
238    #[inline]
239    fn default() -> Self {
240        Builder::new()
241    }
242}