async_rawlogger/
lib.rs

1use crossbeam_channel::{
2    Receiver,
3    RecvTimeoutError,
4    Sender,
5    TrySendError,
6    bounded,
7    unbounded, //
8};
9pub use log::{
10    Level,
11    LevelFilter,
12    Record,
13    debug,
14    error,
15    info,
16    log,
17    log_enabled,
18    logger,
19    trace,
20    warn,
21    //
22};
23use log::{
24    Log,
25    Metadata,
26    SetLoggerError,
27    set_boxed_logger,
28    set_max_level, //
29};
30use logmsg::LogMsg;
31use std::{
32    borrow::Cow,
33    fmt::Display,
34    io::Error as IoError,
35    sync::atomic::{
36        AtomicBool,
37        Ordering, //
38    },
39    time::{Duration, Instant},
40};
41mod logmsg;
42
43enum LoggerInput {
44    LogMsg(LogMsg),
45    Flush,
46}
47
48enum LoggerOutput {
49    Flushed,
50}
51
52pub trait FtLogFormat: Send + Sync {
53    /// turn an reference to record into a box object, which can be sent to log thread
54    /// and then formatted into string.
55    fn msg(
56        &self,
57        record: &Record,
58    ) -> Box<dyn Send + Sync + Display>;
59}
60
61pub struct FtLogFormatter;
62impl FtLogFormat for FtLogFormatter {
63    /// Return a box object that contains required data (e.g. thread name, line of code, etc.) for later formatting into string
64    #[inline]
65    fn msg(
66        &self,
67        record: &Record,
68    ) -> Box<dyn Send + Sync + Display> {
69        Box::new(Message {
70            level: record.level(),
71            file: record
72                .file_static()
73                .map(Cow::Borrowed)
74                .or_else(|| record.file().map(|s| Cow::Owned(s.to_owned())))
75                .unwrap_or(Cow::Borrowed("")),
76            line: record.line(),
77            args: record
78                .args()
79                .as_str()
80                .map(Cow::Borrowed)
81                .unwrap_or_else(|| Cow::Owned(record.args().to_string())),
82        })
83    }
84}
85
86struct Message {
87    level: Level,
88    file: Cow<'static, str>,
89    line: Option<u32>,
90    args: Cow<'static, str>,
91}
92
93impl Display for Message {
94    fn fmt(
95        &self,
96        f: &mut std::fmt::Formatter<'_>,
97    ) -> std::fmt::Result {
98        f.write_str(&format!(
99            "{} [{}:{}] {}",
100            self.level,
101            self.file,
102            self.line.map_or(0, |f| f),
103            self.args
104        ))
105    }
106}
107
108/// A guard that flushes logs associated to a Logger on a drop
109///
110/// With this guard, you can ensure all logs are written to destination
111/// when the application exits.
112pub struct LoggerGuard {
113    queue: Sender<LoggerInput>,
114    notification: Receiver<LoggerOutput>,
115}
116impl Drop for LoggerGuard {
117    fn drop(&mut self) {
118        self.queue
119            .send(LoggerInput::Flush)
120            .expect("logger queue closed when flushing, this is a bug");
121        self.notification
122            .recv()
123            .expect("logger notification closed, this is a bug");
124    }
125}
126/// global logger
127pub struct Logger {
128    format: Box<dyn FtLogFormat>,
129    level: LevelFilter,
130    queue: Sender<LoggerInput>,
131    notification: Receiver<LoggerOutput>,
132    stopped: AtomicBool,
133}
134
135impl Logger {
136    pub fn init(self) -> Result<LoggerGuard, SetLoggerError> {
137        let guard = LoggerGuard {
138            queue: self.queue.clone(),
139            notification: self.notification.clone(),
140        };
141
142        set_max_level(self.level);
143        let boxed = Box::new(self);
144        set_boxed_logger(boxed).map(|_| guard)
145    }
146}
147
148impl Log for Logger {
149    #[inline]
150    fn enabled(
151        &self,
152        metadata: &Metadata,
153    ) -> bool {
154        self.level >= metadata.level()
155    }
156
157    fn log(
158        &self,
159        record: &Record,
160    ) {
161        let msg = self.format.msg(record);
162        let msg = LoggerInput::LogMsg(LogMsg {
163            time: std::time::SystemTime::now(),
164            msg,
165        });
166        match self.queue.try_send(msg) {
167            Err(TrySendError::Full(_)) => {}
168            Err(TrySendError::Disconnected(_)) => {
169                let stop = self.stopped.load(Ordering::SeqCst);
170                if !stop {
171                    eprintln!("logger queue closed when logging, this is a bug");
172                    self.stopped.store(true, Ordering::SeqCst)
173                }
174            }
175            _ => (),
176        }
177    }
178
179    fn flush(&self) {
180        let _ = self
181            .queue
182            .send(LoggerInput::Flush)
183            .map_err(|e| eprintln!("logger queue closed when flushing, this is a bug: {e}"));
184    }
185}
186
187pub struct Builder {
188    format: Box<dyn FtLogFormat>,
189    level: Option<LevelFilter>,
190    root_level: Option<LevelFilter>,
191}
192
193/// Handy function to get ftlog builder
194#[inline]
195pub fn builder() -> Builder {
196    Builder::new()
197}
198
199impl Builder {
200    #[inline]
201    pub fn new() -> Builder {
202        Builder {
203            format: Box::new(FtLogFormatter),
204            level: None,
205            root_level: None,
206        }
207    }
208
209    /// Set custom formatter
210    #[inline]
211    pub fn format<F: FtLogFormat + 'static>(
212        mut self,
213        format: F,
214    ) -> Builder {
215        self.format = Box::new(format);
216        self
217    }
218
219    #[inline]
220    /// Set max log level
221    /// Logs with level more verbose than this will not be sent to log thread.
222    pub fn max_log_level(
223        mut self,
224        level: LevelFilter,
225    ) -> Builder {
226        self.level = Some(level);
227        self
228    }
229
230    #[inline]
231    /// Set max log level
232    ///
233    /// Logs with level more verbose than this will not be sent to log thread.
234    pub fn root_log_level(
235        mut self,
236        level: LevelFilter,
237    ) -> Builder {
238        self.root_level = Some(level);
239        self
240    }
241
242    /// Finish building ftlog logger
243    ///
244    /// The call spawns a log thread to formatting log message into string,
245    /// and write to output target.
246    pub fn build(self) -> Result<Logger, IoError> {
247        let global_level = self.level.unwrap_or(LevelFilter::Info);
248        let root_level = self.root_level.unwrap_or(global_level);
249        if global_level < root_level {
250            warn!("Logs with level more verbose than {global_level} will be ignored");
251        }
252
253        let (sync_sender, receiver) = unbounded();
254        let (notification_sender, notification_receiver) = bounded(1);
255        std::thread::Builder::new()
256            .name("logger".to_string())
257            .spawn(move || {
258                let mut last_flush = Instant::now();
259                let timeout = Duration::from_millis(100);
260                loop {
261                    match receiver.recv_timeout(timeout) {
262                        Ok(LoggerInput::LogMsg(msg)) => {
263                            msg.write();
264                        }
265                        Ok(LoggerInput::Flush) => {
266                            let max = receiver.len();
267                            'queue: for _ in 1..=max {
268                                if let Ok(LoggerInput::LogMsg(msg)) = receiver.try_recv() {
269                                    msg.write();
270                                } else {
271                                    break 'queue;
272                                }
273                            }
274                            notification_sender
275                                .send(LoggerOutput::Flushed)
276                                .expect("logger notification failed");
277                        }
278                        Err(RecvTimeoutError::Timeout) => {
279                            if last_flush.elapsed() > Duration::from_millis(1000) {
280                                last_flush = Instant::now();
281                            }
282                        }
283                        Err(e) => {
284                            eprintln!(
285                                "sender closed without sending a Quit first, this is a bug, {e}"
286                            );
287                        }
288                    }
289                }
290            })?;
291        Ok(Logger {
292            format: self.format,
293            level: global_level,
294            queue: sync_sender,
295            notification: notification_receiver,
296            stopped: AtomicBool::new(false),
297        })
298    }
299
300    /// try building and setting as global logger
301    pub fn try_init(self) -> Result<LoggerGuard, Box<dyn std::error::Error>> {
302        let logger = self.build()?;
303        Ok(logger.init()?)
304    }
305}
306
307impl Default for Builder {
308    #[inline]
309    fn default() -> Self {
310        Builder::new()
311    }
312}