ftlog/
lib.rs

1//! [![Build Status](https://github.com/nonconvextech/ftlog/workflows/ftlog/badge.svg?branch=main)](https://github.com/nonconvextech/ftlog/actions)
2//! ![License](https://img.shields.io/crates/l/ftlog.svg)
3//! [![Latest Version](https://img.shields.io/crates/v/ftlog.svg)](https://crates.io/crates/ftlog)
4//! [![ftlog](https://docs.rs/ftlog/badge.svg)](https://docs.rs/ftlog)
5//!
6//! Logging is affected by the disk IO and pipe system call.
7//! Sequential log calls can be a bottleneck in scenarios where low
8//! latency is critical (e.g., high-frequency trading).
9//!
10//! `ftlog` mitigates this bottleneck by sending messages to a dedicated logger
11//! thread and computing as little as possible in the main/worker thread.
12//!
13//! `ftlog` can improve log performance in main/worker thread a few times over. See
14//! performance for details.
15//!
16//!
17//! # Usage
18//!
19//! Add to your `Cargo.toml`:
20//!
21//! ```toml
22//! ftlog = "0.2"
23//! ```
24//!
25//! Configure and initialize ftlog at the start of your `main` function:
26//! ```
27//! // ftlog re-export `log`'s macros, so no need to add `log` to dependencies
28//! use ftlog::appender::FileAppender;
29//! use ftlog::{debug, trace};
30//! use log::{error, info, warn};
31//!
32//! // minimal configuration with default setting
33//!
34//! // When drops, the guard calls and waits `flush` to logger.
35//! // With guard that share the lifetime of `main` fn, there is no need to manually call flush at the end of `main` fn.
36//! let _guard = ftlog::builder().try_init().unwrap();
37//!
38//! trace!("Hello world!");
39//! debug!("Hello world!");
40//! info!("Hello world!");
41//! warn!("Hello world!");
42//! error!("Hello world!");
43//! ```
44//!
45//! A more complicated but feature rich usage:
46//!
47//! ```rust,no_run
48//! use ftlog::{
49//!     appender::{Duration, FileAppender, Period},
50//!     FtLogFormatter, LevelFilter,
51//! };
52//!
53//! let time_format = time::format_description::parse_owned::<1>(
54//!     "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:6]",
55//! )
56//! .unwrap();
57//! // configurate logger
58//! let _guard = ftlog::builder()
59//!     // global max log level
60//!     .max_log_level(LevelFilter::Info)
61//!     // custom timestamp format
62//!     .time_format(time_format)
63//!     // set global log formatter
64//!     .format(FtLogFormatter)
65//!     // use bounded channel to avoid large memory comsumption when overwhelmed with logs
66//!     // Set `false` to tell ftlog to discard excessive logs.
67//!     // Set `true` to block log call to wait for log thread.
68//!     // here is the default settings
69//!     .bounded(100_000, false) // .unbounded()
70//!     // define root appender, pass anything that is Write and Send
71//!     // omit `Builder::root` will write to stderr
72//!     .root(
73//!         FileAppender::builder()
74//!             .path("./current.log")
75//!             .rotate(Period::Day)
76//!             .expire(Duration::days(7))
77//!             .build(),
78//!     )
79//!     // timezone of log message timestamp, use local by default
80//!     // .local_timezone()
81//!     // or use fiexed timezone for better throughput, since retrieving timezone is a time consuming operation
82//!     // this does not affect worker threads (that call log), but can boost log thread performance (higher throughput).
83//!     .fixed_timezone(time::UtcOffset::current_local_offset().unwrap())
84//!     // level filter for root appender
85//!     .root_log_level(LevelFilter::Warn)
86//!     // write logs in ftlog::appender to "./ftlog-appender.log" instead of "./current.log"
87//!     .filter("ftlog::appender", "ftlog-appender", LevelFilter::Error)
88//!     .appender("ftlog-appender", FileAppender::new("ftlog-appender.log"))
89//!     .try_init()
90//!     .expect("logger build or set failed");
91//! ```
92//!
93//! See `./examples` for more (e.g. custom format).
94//!
95//! ## Default Log Format
96//!
97//! > 2022-04-08 19:20:48.190+08 **298ms** INFO main [src/ftlog.rs:14] My log
98//! > message
99//!
100//! Here `298ms` denotes the latency between the call of the log (e.g.
101//! `log::info!("msg")`) and the actual printing in log thread. Normally this is 0ms.
102//!
103//! A large delay indicates that the log thread may be blocked by excessive log
104//! messages.
105//!
106//! > 2022-04-10 21:27:15.996+08 0ms **2** INFO main [src/main.rs:29] limit
107//! > running3 !
108//!
109//! The number **2** above indicates how many log messages were discarded.
110//! Only shown if the frequency of logging for a single log call is limited (e.g.
111//! `log::info!(limit=3000i64;"msg")`).
112//!
113//! ## Randomly drop log
114//!
115//! Use `random_drop` or `drop` to specify the probability of randomly discarding logs.
116//! No message is dropped by default.
117//!
118//! ```rust
119//! log::info!(random_drop=0.1f32;"Random log 10% of log calls, keeps 90%");
120//! log::info!(drop=0.99f32;"Random drop 99% of log calls, keeps 1%");
121//! ```
122//!
123//! This can be helpful when formatting log message into string is too costly,
124//!
125//! When both `random_drop` and `limit` is specified,
126//! ftlog will limit logs after messages are randomly dropped.
127//! ```rust
128//! log::info!(drop=0.99f32,limit=1000;
129//!     "Drop 99% messages. The survived 1% messages are limit to at least 1000ms between adjacent log messages output"
130//! );
131//! ```
132//!
133//! ## Custom timestamp format
134//!
135//! `ftlog` relies on the `time` crate for the formatting of timestamp. To use custom time format,
136//! first construct a valid time format description,
137//! and then pass it to ftlog builder by `ftlog::time_format(&mut self)`.
138//!
139//! In case an error occurs when formatting timestamp, `ftlog` will fallback to RFC3339 time format.
140//!
141//! ### Example
142//! ```rust
143//! let format = time::format_description::parse_owned::<1>(
144//!     "[year]/[month]/[day] [hour]:[minute]:[second].[subsecond digits:6]",
145//! )
146//! .unwrap();
147//! let _guard = ftlog::builder().time_format(format).try_init().unwrap();
148//! log::info!("Log with custom timestamp format");
149//! // Output:
150//! // 2023/06/14 11:13:26.160840 0ms INFO main [main.rs:3] Log with custom timestamp format
151//! ```
152//!
153//! ## Log with interval
154//!
155//! `ftlog` allows to limit the write frequency for individual log calls.
156//!
157//! If the above line is called multiple times within 3000ms, then it is logged only
158//! once, with an added number reflecting the number of discarded log messages.
159//!
160//! Each log call ha an independent interval, so we can set different intervals
161//! for different log calls. Internally, `ftlog` records the last print time by a
162//! combination of (module name, file name, code line).
163//!
164//! ### Example
165//!
166//! ```rust
167//! # use ftlog::info;
168//! info!(limit=3000i64; "limit running {}s !", 3);
169//! ```
170//! The minimal interval of the the specific log call above is 3000ms.
171//!
172//! ```markdown
173//! 2022-04-10 21:27:10.996+08 0ms 0 INFO main [src/main.rs:29] limit running 3s !
174//! 2022-04-10 21:27:15.996+08 0ms 2 INFO main [src/main.rs:29] limit running 3s !
175//! ```
176//! The number **2** above shows how many log messages is discarded since last log.
177//!
178//! ## Log rotation
179//! `ftlog` supports log rotation in local timezone. The available rotation
180//! periods are:
181//!
182//! - minute `Period::Minute`
183//! - hour `Period::Hour`
184//! - day `Period::Day`
185//! - month `Period::Month`
186//! - year `Period::Year`
187//!
188//! Log rotation is configured in `FileAppender`, and the timestamp is appended to
189//! the end of the filename:
190//!
191//! ```rust
192//! use ftlog::appender::{FileAppender, Period};
193//!
194//! let logger = ftlog::builder()
195//!     .root(
196//!         FileAppender::builder()
197//!             .path("./mylog.log")
198//!             .rotate(Period::Minute)
199//!             .build(),
200//!     )
201//!     .build()
202//!     .unwrap();
203//! let _guard = logger.init().unwrap();
204//! ```
205//!
206//! If the log file is configured to be split by minutes,
207//! the log file name has the format
208//! `mylog-{MMMM}{YY}{DD}T{hh}{mm}.log`. When divided by days, the log file name is
209//! something like `mylog-{MMMM}{YY}{DD}.log`.
210//!
211//! Log filename examples:
212//! ```sh
213//! $ ls
214//! # by minute
215//! current-20221026T1351.log
216//! # by hour
217//! current-20221026T13.log
218//! # by day
219//! current-20221026.log
220//! # by month
221//! current-202210.log
222//! # by year
223//! current-2022.log
224//! # omitting extension (e.g. "./log") will add datetime to the end of log filename
225//! log-20221026T1351
226//! ```
227//!
228//! ### Clean outdated logs
229//!
230//! With log rotation enabled, it is possible to clean outdated logs to free up
231//! disk space with `FileAppender::rotate_with_expire` method or set `expire(Duration)`
232//! when using builder.
233//!
234//! `ftlog` first finds files generated by `ftlog` and cleans outdated logs by
235//! last modified time. `ftlog` find generated logs by filename matched by file
236//! stem and added datetime.
237//!
238//! **ATTENTION**: Any files that matchs the pattern will be deleted.
239//!
240//! ```rust
241//! use ftlog::{appender::{Period, FileAppender, Duration}};
242//!
243//! // clean files named like `current-\d{8}T\d{4}.log`.
244//! // files like `another-\d{8}T\d{4}.log` or `current-\d{8}T\d{4}` will not be deleted, since the filenames' stem do not match.
245//! // files like `current-\d{8}.log` will remains either, since the rotation durations do not match.
246//!
247//! // Rotate every day, clean stale logs that were modified 7 days ago on each rotation
248//! let appender = FileAppender::rotate_with_expire("./current.log", Period::Day, Duration::days(7));
249//! let logger = ftlog::builder()
250//!     .root(appender)
251//!     .build()
252//!     .unwrap();
253//! let _guard = logger.init().unwrap();
254//! ```
255//!
256//! # Features
257//! - **tsc**
258//!   Use [TSC](https://en.wikipedia.org/wiki/Time_Stamp_Counter) for clock source for higher performance without
259//!   accuracy loss.
260//!   
261//!   TSC offers the most accurate and cheapest way to access current time under certain condition:
262//!   1. the CPU frequency must be constant
263//!   1. must with CPU of x86/x86_64 architecture, since TSC is an x86/x86_64 specific register.
264//!   1. never suspend
265//!
266//!   The current feature further requires that the build target **MUST BE LINUX**. Otherwise it will fall back to
267//!   a fast but much less accurate implementation.
268//!   
269//! # Timezone
270//!
271//! For performance, timezone is detected once at logger buildup, and use it later in every
272//! log message. This is partly due to timezone detetion is expensive, and partly to the unsafe
273//! nature of underlying system call in multi-thread program in Linux.
274//!
275//! It's also recommended to use UTC instead to further avoid timestamp convertion to timezone for every log message.
276//!
277//! # Performance
278//!
279//! > Rust:1.67.0-nightly
280//!
281//! |                                                   |  message type | Apple M1 Pro, 3.2GHz  | AMD EPYC 7T83, 3.2GHz |
282//! | ------------------------------------------------- | ------------- | --------------------- | --------------------- |
283//! | `ftlog`                                           | static string |   75 ns/iter    | 385 ns/iter    |
284//! | `ftlog`                                           | with i32      |   106 ns/iter   | 491 ns/iter    |
285//! | `env_logger` <br/> output to file                 | static string | 1,674 ns/iter  | 1,142 ns/iter   |
286//! | `env_logger` <br/> output to file                 | with i32      | 1,681 ns/iter   | 1,179 ns/iter   |
287//! | `env_logger` <br/> output to file with `BufWriter`| static string | 279 ns/iter     | 550 ns/iter     |
288//! | `env_logger` <br/> output to file with `BufWriter`| with i32      | 278 ns/iter     | 565 ns/iter     |
289
290use arc_swap::ArcSwap;
291pub use log::{
292    debug, error, info, log, log_enabled, logger, trace, warn, Level, LevelFilter, Record,
293};
294use time::format_description::OwnedFormatItem;
295use time::{OffsetDateTime, UtcOffset};
296
297use std::borrow::Cow;
298use std::fmt::Display;
299use std::hash::{BuildHasher, Hash, Hasher};
300use std::io::{stderr, Error as IoError, Write};
301use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
302use std::sync::Arc;
303use std::time::{Duration, Instant};
304
305use crossbeam_channel::{bounded, unbounded, Receiver, RecvTimeoutError, Sender, TrySendError};
306use hashbrown::HashMap;
307use log::{kv::Key, set_boxed_logger, set_max_level, Log, Metadata, SetLoggerError};
308
309pub mod appender;
310
311use tm::{duration, now, to_utc, Time};
312
313#[cfg(not(feature = "tsc"))]
314mod tm {
315    use super::*;
316
317    pub type Time = std::time::SystemTime;
318    #[inline]
319    pub fn now() -> Time {
320        std::time::SystemTime::now()
321    }
322    #[inline]
323    pub fn to_utc(time: Time) -> OffsetDateTime {
324        time.into()
325    }
326
327    #[inline]
328    pub fn duration(from: Time, to: Time) -> Duration {
329        to.duration_since(from).unwrap_or_default()
330    }
331}
332
333#[cfg(feature = "tsc")]
334mod tm {
335    use super::*;
336    pub type Time = minstant::Instant;
337    #[inline]
338    pub fn now() -> Time {
339        minstant::Instant::now()
340    }
341    #[inline]
342    pub fn to_utc(time: Time) -> OffsetDateTime {
343        static ANCHOR: once_cell::sync::Lazy<minstant::Anchor> =
344            once_cell::sync::Lazy::new(|| minstant::Anchor::new());
345        OffsetDateTime::from_unix_timestamp_nanos(time.as_unix_nanos(&ANCHOR) as i128).unwrap()
346    }
347    #[inline]
348    pub fn duration(from: Time, to: Time) -> Duration {
349        to.duration_since(from)
350    }
351}
352
353#[cfg(target_family = "unix")]
354fn local_timezone() -> UtcOffset {
355    UtcOffset::current_local_offset().unwrap_or_else(|_| {
356        let tz = tz::TimeZone::local().unwrap();
357        let current_local_time_type = tz.find_current_local_time_type().unwrap();
358        let diff_secs = current_local_time_type.ut_offset();
359        UtcOffset::from_whole_seconds(diff_secs).unwrap()
360    })
361}
362#[cfg(not(target_family = "unix"))]
363fn local_timezone() -> UtcOffset {
364    UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC)
365}
366
367struct LogMsg {
368    time: Time,
369    msg: Box<dyn Sync + Send + Display>,
370    level: Level,
371    target: String,
372    limit: u32,
373    limit_key: u64,
374}
375impl LogMsg {
376    fn write(
377        self,
378        filters: &Vec<Directive>,
379        appenders: &mut HashMap<&'static str, Box<dyn Write + Send>>,
380        root: &mut Box<dyn Write + Send>,
381        root_level: LevelFilter,
382        missed_log: &mut HashMap<u64, i64, nohash_hasher::BuildNoHashHasher<u64>>,
383        last_log: &mut HashMap<u64, Time, nohash_hasher::BuildNoHashHasher<u64>>,
384        offset: Option<UtcOffset>,
385        time_format: &time::format_description::OwnedFormatItem,
386    ) {
387        let writer = if let Some(filter) = filters.iter().find(|x| self.target.starts_with(x.path))
388        {
389            if filter.level.map(|l| l < self.level).unwrap_or(false) {
390                return;
391            }
392            filter
393                .appender
394                .and_then(|n| appenders.get_mut(n))
395                .unwrap_or(root)
396        } else {
397            if root_level < self.level {
398                return;
399            }
400            root
401        };
402
403        let msg = self.msg.to_string();
404        if msg.is_empty() {
405            return;
406        }
407
408        let now = now();
409
410        if self.limit > 0 {
411            let missed_entry = missed_log.entry(self.limit_key).or_insert_with(|| 0);
412            if let Some(last) = last_log.get(&self.limit_key) {
413                if duration(*last, now) < Duration::from_millis(self.limit as u64) {
414                    *missed_entry += 1;
415                    return;
416                }
417            }
418            last_log.insert(self.limit_key, now);
419            let delay = duration(self.time, now);
420            let utc_datetime = to_utc(self.time);
421
422            let offset_datetime = offset
423                .map(|o| utc_datetime.to_offset(o))
424                .unwrap_or(utc_datetime);
425
426            let s = format!(
427                "{} {}ms {} {}\n",
428                offset_datetime
429                    .format(&time_format)
430                    .unwrap_or_else(|_| offset_datetime
431                        .format(&time::format_description::well_known::Rfc3339)
432                        .unwrap()),
433                delay.as_millis(),
434                *missed_entry,
435                msg
436            );
437            if let Err(e) = writer.write_all(s.as_bytes()) {
438                eprintln!("logger write message failed: {}", e);
439            };
440            *missed_entry = 0;
441        } else {
442            let delay = duration(self.time, now);
443            let utc_datetime = to_utc(self.time);
444            let offset_datetime = offset
445                .map(|o| utc_datetime.to_offset(o))
446                .unwrap_or(utc_datetime);
447            let s = format!(
448                "{} {}ms {}\n",
449                offset_datetime
450                    .format(&time_format)
451                    .unwrap_or_else(|_| offset_datetime
452                        .format(&time::format_description::well_known::Rfc3339)
453                        .unwrap()),
454                delay.as_millis(),
455                msg
456            );
457            if let Err(e) = writer.write_all(s.as_bytes()) {
458                eprintln!("logger write message failed: {}", e);
459            };
460        }
461    }
462}
463
464enum LoggerInput {
465    LogMsg(LogMsg),
466    Flush,
467}
468
469#[derive(Debug)]
470enum LoggerOutput {
471    Flushed,
472    FlushError(std::io::Error),
473}
474
475/// Shared by ftlog formatter
476///
477/// To further reduce time spent on log macro calls, ftlog saves required data
478/// and later construct log string in log thread.
479///
480/// `FtLogFormat` defines how to turn an reference to record into a box object,
481/// which can be sent to log thread and later formatted into string.
482///
483/// Here is an example of custom formatter:
484///
485/// ```
486/// use std::fmt::Display;
487///
488/// use ftlog::FtLogFormat;
489/// use log::{Level, Record};
490///
491/// struct MyFormatter;
492/// impl FtLogFormat for MyFormatter {
493///     fn msg(&self, record: &Record) -> Box<dyn Send + Sync + Display> {
494///         Box::new(Msg {
495///             level: record.level(),
496///             thread: std::thread::current().name().map(|n| n.to_string()),
497///             file: record.file_static(),
498///             line: record.line(),
499///             args: format!("{}", record.args()),
500///             module_path: record.module_path_static(),
501///         })
502///     }
503/// }
504/// // Store necessary field, define how to format into string with `Display` trait.
505/// struct Msg {
506///     level: Level,
507///     thread: Option<String>,
508///     file: Option<&'static str>,
509///     line: Option<u32>,
510///     args: String,
511///     module_path: Option<&'static str>,
512/// }
513///
514/// impl Display for Msg {
515///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516///         f.write_str(&format!(
517///             "{}@{}||{}:{}[{}] {}",
518///             self.thread.as_ref().map(|x| x.as_str()).unwrap_or(""),
519///             self.module_path.unwrap_or(""),
520///             self.file.unwrap_or(""),
521///             self.line.unwrap_or(0),
522///             self.level,
523///             self.args
524///         ))
525///     }
526/// }
527/// ```
528pub trait FtLogFormat: Send + Sync {
529    /// turn an reference to record into a box object, which can be sent to log thread
530    /// and then formatted into string.
531    fn msg(&self, record: &Record) -> Box<dyn Send + Sync + Display>;
532}
533
534/// Default ftlog formatter
535///
536/// The default ftlog format is like:
537/// ```text
538/// INFO main [examples/ftlog.rs:27] Hello, world!
539/// ```
540///
541/// Since ftlog cannot customize timestamp, the corresponding part is omitted.
542/// The actual log output is like:
543/// ```text
544/// 2022-11-22 17:02:12.574+08 0ms INFO main [examples/ftlog.rs:27] Hello, world!
545/// ```
546pub struct FtLogFormatter;
547impl FtLogFormat for FtLogFormatter {
548    /// Return a box object that contains required data (e.g. thread name, line of code, etc.) for later formatting into string
549    #[inline]
550    fn msg(&self, record: &Record) -> Box<dyn Send + Sync + Display> {
551        Box::new(Message {
552            level: record.level(),
553            thread: std::thread::current().name().map(|n| n.to_string()),
554            file: record
555                .file_static()
556                .map(|s| Cow::Borrowed(s))
557                .or_else(|| record.file().map(|s| Cow::Owned(s.to_owned())))
558                .unwrap_or(Cow::Borrowed("")),
559            line: record.line(),
560            args: record
561                .args()
562                .as_str()
563                .map(|s| Cow::Borrowed(s))
564                .unwrap_or_else(|| Cow::Owned(format!("{}", record.args()))),
565        })
566    }
567}
568
569struct Message {
570    level: Level,
571    thread: Option<String>,
572    file: Cow<'static, str>,
573    line: Option<u32>,
574    args: Cow<'static, str>,
575}
576
577impl Display for Message {
578    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
579        f.write_str(&format!(
580            "{} {} [{}:{}] {}",
581            self.level,
582            self.thread.as_ref().map(|x| x.as_str()).unwrap_or(""),
583            self.file,
584            self.line.unwrap_or(0),
585            self.args
586        ))
587    }
588}
589
590struct DiscardState {
591    last: ArcSwap<Instant>,
592    count: AtomicUsize,
593}
594
595/// A guard that flushes logs associated to a Logger on a drop
596///
597/// With this guard, you can ensure all logs are written to destination
598/// when the application exits.
599pub struct LoggerGuard {
600    queue: Sender<LoggerInput>,
601    notification: Receiver<LoggerOutput>,
602}
603impl Drop for LoggerGuard {
604    fn drop(&mut self) {
605        self.queue
606            .send(LoggerInput::Flush)
607            .expect("logger queue closed when flushing, this is a bug");
608        self.notification
609            .recv()
610            .expect("logger notification closed, this is a bug");
611    }
612}
613/// ftlog global logger
614pub struct Logger {
615    format: Box<dyn FtLogFormat>,
616    level: LevelFilter,
617    queue: Sender<LoggerInput>,
618    notification: Receiver<LoggerOutput>,
619    block: bool,
620    discard_state: Option<DiscardState>,
621    stopped: AtomicBool,
622}
623
624impl Logger {
625    pub fn init(self) -> Result<LoggerGuard, SetLoggerError> {
626        let guard = LoggerGuard {
627            queue: self.queue.clone(),
628            notification: self.notification.clone(),
629        };
630
631        set_max_level(self.level);
632        let boxed = Box::new(self);
633        set_boxed_logger(boxed).map(|_| guard)
634    }
635}
636
637impl Log for Logger {
638    #[inline]
639    fn enabled(&self, metadata: &Metadata) -> bool {
640        // already checked in log macros
641        self.level >= metadata.level()
642    }
643
644    fn log(&self, record: &Record) {
645        #[cfg(feature = "random_drop")]
646        {
647            let random_drop = record
648                .key_values()
649                .get(Key::from_str("random_drop"))
650                .or_else(|| record.key_values().get(Key::from_str("drop")))
651                .and_then(|x| x.to_f64())
652                .unwrap_or(1.) as f32;
653            if random_drop < 1. && fastrand::f32() < random_drop {
654                return;
655            }
656        }
657
658        let limit = record
659            .key_values()
660            .get(Key::from_str("limit"))
661            .and_then(|x| x.to_u64())
662            .unwrap_or(0) as u32;
663
664        let msg = self.format.msg(record);
665        let limit_key = if limit == 0 {
666            0
667        } else {
668            let mut b = hashbrown::hash_map::DefaultHashBuilder::default().build_hasher();
669            if let Some(p) = record.module_path() {
670                p.as_bytes().hash(&mut b);
671            } else {
672                record.file().unwrap_or("").as_bytes().hash(&mut b);
673            }
674            record.line().unwrap_or(0).hash(&mut b);
675            b.finish()
676        };
677        let msg = LoggerInput::LogMsg(LogMsg {
678            time: now(),
679            msg: msg,
680            target: record.target().to_owned(),
681            level: record.level(),
682            limit,
683            limit_key,
684        });
685        if self.block {
686            if let Err(_) = self.queue.send(msg) {
687                let stop = self.stopped.load(Ordering::SeqCst);
688                if !stop {
689                    eprintln!("logger queue closed when logging, this is a bug");
690                    self.stopped.store(true, Ordering::SeqCst)
691                }
692            }
693        } else {
694            match self.queue.try_send(msg) {
695                Err(TrySendError::Full(_)) => {
696                    if let Some(s) = &self.discard_state {
697                        let count = s.count.fetch_add(1, Ordering::SeqCst);
698                        if s.last.load().elapsed().as_secs() >= 5 {
699                            eprintln!("Excessive log messages. Log omitted: {}", count);
700                            s.last.store(Arc::new(Instant::now()));
701                        }
702                    }
703                }
704                Err(TrySendError::Disconnected(_)) => {
705                    let stop = self.stopped.load(Ordering::SeqCst);
706                    if !stop {
707                        eprintln!("logger queue closed when logging, this is a bug");
708                        self.stopped.store(true, Ordering::SeqCst)
709                    }
710                }
711                _ => (),
712            }
713        }
714    }
715
716    fn flush(&self) {
717        self.queue
718            .send(LoggerInput::Flush)
719            .expect("logger queue closed when flushing, this is a bug");
720        if let LoggerOutput::FlushError(err) = self
721            .notification
722            .recv()
723            .expect("logger notification closed, this is a bug")
724        {
725            eprintln!("Fail to flush: {}", err);
726        }
727    }
728}
729
730struct BoundedChannelOption {
731    size: usize,
732    block: bool,
733    print: bool,
734}
735
736/// Ftlog builder
737///
738/// ```
739/// # use ftlog::appender::{FileAppender, Duration, Period};
740/// # use log::LevelFilter;
741/// let logger = ftlog::builder()
742///     // use our own format
743///     .format(ftlog::FtLogFormatter)
744///     // global max log level
745///     .max_log_level(LevelFilter::Info)
746///     // define root appender, pass anything that is Write and Send
747///     // omit `Builder::root` to write to stderr
748///     .root(FileAppender::rotate_with_expire(
749///         "./current.log",
750///         Period::Day,
751///         Duration::days(7),
752///     ))
753///     // ---------- configure additional filter ----------
754///     // write to "ftlog-appender" appender, with different level filter
755///     .filter("ftlog::appender", "ftlog-appender", LevelFilter::Error)
756///     // write to root appender, but with different level filter
757///     .filter("ftlog", None, LevelFilter::Trace)
758///     // write to "ftlog" appender, with default level filter
759///     .filter("ftlog::appender::file", "ftlog", None)
760///     // ----------  configure additional appender ----------
761///     // new appender
762///     .appender("ftlog-appender", FileAppender::new("ftlog-appender.log"))
763///     // new appender, rotate to new file every Day
764///     .appender("ftlog", FileAppender::rotate("ftlog.log", Period::Day))
765///     .build()
766///     .expect("logger build failed");
767/// ```
768///
769/// # Local timezone
770/// For performance reason, `ftlog` only retrieve timezone info once and use this
771/// local timezone offset forever. Thus timestamp in log does not aware of timezone
772/// change by OS.
773pub struct Builder {
774    format: Box<dyn FtLogFormat>,
775    time_format: Option<OwnedFormatItem>,
776    level: Option<LevelFilter>,
777    root_level: Option<LevelFilter>,
778    root: Box<dyn Write + Send>,
779    appenders: HashMap<&'static str, Box<dyn Write + Send + 'static>>,
780    filters: Vec<Directive>,
781    bounded_channel_option: Option<BoundedChannelOption>,
782    timezone: LogTimezone,
783}
784
785/// Handy function to get ftlog builder
786#[inline]
787pub fn builder() -> Builder {
788    Builder::new()
789}
790
791struct Directive {
792    path: &'static str,
793    level: Option<LevelFilter>,
794    appender: Option<&'static str>,
795}
796/// timezone for log
797pub enum LogTimezone {
798    /// local timezone
799    ///
800    /// Only *unix OS is supported for now
801    Local,
802    /// UTC timezone
803    Utc,
804    /// fixed timezone
805    Fixed(UtcOffset),
806}
807
808impl Builder {
809    #[inline]
810    /// Create a ftlog builder with default settings:
811    /// - global log level: INFO
812    /// - root log level: INFO
813    /// - default formatter: `FtLogFormatter`
814    /// - output to stderr
815    /// - bounded channel between worker thread and log thread, with a size limit of 100_000
816    /// - discard excessive log messages
817    /// - log with timestamp of local timezone
818    pub fn new() -> Builder {
819        Builder {
820            format: Box::new(FtLogFormatter),
821            level: None,
822            root_level: None,
823            root: Box::new(stderr()) as Box<dyn Write + Send>,
824            appenders: HashMap::new(),
825            filters: Vec::new(),
826            bounded_channel_option: Some(BoundedChannelOption {
827                size: 100_000,
828                block: false,
829                print: true,
830            }),
831            timezone: LogTimezone::Local,
832            time_format: None,
833        }
834    }
835
836    /// Set custom formatter
837    #[inline]
838    pub fn format<F: FtLogFormat + 'static>(mut self, format: F) -> Builder {
839        self.format = Box::new(format);
840        self
841    }
842
843    /// Set custom datetime formatter
844    #[inline]
845    pub fn time_format(mut self, format: OwnedFormatItem) -> Builder {
846        self.time_format = Some(format);
847        self
848    }
849
850    /// bound channel between worker thread and log thread
851    ///
852    /// When `block_when_full` is true, it will block current thread where
853    /// log macro (e.g. `log::info`) is called until log thread is able to handle new message.
854    /// Otherwises, excessive log messages will be discarded.
855    ///
856    /// By default, excessive log messages is discarded silently. To show how many log
857    /// messages have been dropped, see `Builder::print_omitted_count()`.
858    #[inline]
859    pub fn bounded(mut self, size: usize, block_when_full: bool) -> Builder {
860        self.bounded_channel_option = Some(BoundedChannelOption {
861            size,
862            block: block_when_full,
863            print: false,
864        });
865        self
866    }
867
868    /// whether to print the number of omitted logs if channel to log
869    /// thread is bounded, and set to discard excessive log messages
870    #[inline]
871    pub fn print_omitted_count(mut self, print: bool) -> Builder {
872        self.bounded_channel_option
873            .as_mut()
874            .map(|o| o.print = print);
875        self
876    }
877
878    /// set channel size to unbound
879    ///
880    /// **ATTENTION**: too much log message will lead to huge memory consumption,
881    /// as log messages are queued to be handled by log thread.
882    /// When log message exceed the current channel size, it will double the size by default,
883    /// Since channel expansion asks for memory allocation, log calls can be slow down.
884    #[inline]
885    pub fn unbounded(mut self) -> Builder {
886        self.bounded_channel_option = None;
887        self
888    }
889
890    /// Add an additional appender with a name
891    ///
892    /// Combine with `Builder::filter()`, ftlog can output log in different module
893    /// path to different output target.
894    #[inline]
895    pub fn appender(
896        mut self,
897        name: &'static str,
898        appender: impl Write + Send + 'static,
899    ) -> Builder {
900        self.appenders.insert(name, Box::new(appender));
901        self
902    }
903
904    /// Add a filter to redirect log to different output
905    /// target (e.g. stderr, stdout, different files).
906    ///
907    /// **ATTENTION**: level more verbose than `Builder::max_log_level` will be ignored.
908    /// Say we configure `max_log_level` to INFO, and even if filter's level is set to DEBUG,
909    /// ftlog will still log up to INFO.
910    #[inline]
911    pub fn filter<A: Into<Option<&'static str>>, L: Into<Option<LevelFilter>>>(
912        mut self,
913        module_path: &'static str,
914        appender: A,
915        level: L,
916    ) -> Builder {
917        let appender = appender.into();
918        let level = level.into();
919        if appender.is_some() || level.is_some() {
920            self.filters.push(Directive {
921                path: module_path,
922                appender: appender,
923                level: level,
924            });
925        }
926        self
927    }
928
929    #[inline]
930    /// Configure the default log output target.
931    ///
932    /// Omit this method will output to stderr.
933    pub fn root(mut self, writer: impl Write + Send + 'static) -> Builder {
934        self.root = Box::new(writer);
935        self
936    }
937
938    #[inline]
939    /// Set max log level
940    ///
941    /// Logs with level more verbose than this will not be sent to log thread.
942    pub fn max_log_level(mut self, level: LevelFilter) -> Builder {
943        self.level = Some(level);
944        self
945    }
946
947    #[inline]
948    /// Set max log level
949    ///
950    /// Logs with level more verbose than this will not be sent to log thread.
951    pub fn root_log_level(mut self, level: LevelFilter) -> Builder {
952        self.root_level = Some(level);
953        self
954    }
955
956    #[inline]
957    /// Log with timestamp of local timezone
958    ///
959    /// Timezone is fixed after logger setup for the following reasons:
960    /// 1. `time` v0.3 currently do not allow access to local offset for multithread process
961    /// in unix-like OS.
962    /// 1. timezone retrieval from OS is quite slow (around several microsecond) compare with
963    /// utc timestamp retrieval (around tens of nanoseconds)
964    pub fn local_timezone(mut self) -> Builder {
965        self.timezone = LogTimezone::Local;
966        self
967    }
968
969    #[inline]
970    /// Log with timestamp of UTC timezone
971    pub fn utc(mut self) -> Builder {
972        self.timezone = LogTimezone::Utc;
973        self
974    }
975
976    #[inline]
977    /// Log with timestamp of fixed timezone
978    pub fn fixed_timezone(mut self, timezone: UtcOffset) -> Builder {
979        self.timezone = LogTimezone::Fixed(timezone);
980        self
981    }
982
983    #[inline]
984    /// Specify the timezone of log messages
985    pub fn timezone(mut self, timezone: LogTimezone) -> Builder {
986        self.timezone = timezone;
987        self
988    }
989
990    /// Finish building ftlog logger
991    ///
992    /// The call spawns a log thread to formatting log message into string,
993    /// and write to output target.
994    pub fn build(self) -> Result<Logger, IoError> {
995        let offset = match self.timezone {
996            LogTimezone::Local => Some(local_timezone()),
997            LogTimezone::Utc => None,
998            LogTimezone::Fixed(offset) => Some(offset),
999        };
1000        let time_format = self.time_format.unwrap_or_else(|| {
1001            time::format_description::parse_owned::<1>(
1002                "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]+[offset_hour]",
1003            )
1004            .unwrap()
1005        });
1006        let mut filters = self.filters;
1007        // sort filters' paths to ensure match for longest path
1008        filters.sort_by(|a, b| a.path.len().cmp(&b.path.len()));
1009        filters.reverse();
1010        // check appender name in filters are all valid
1011        for appender_name in filters.iter().filter_map(|x| x.appender) {
1012            if !self.appenders.contains_key(appender_name) {
1013                panic!("Appender {} not configured", appender_name);
1014            }
1015        }
1016        let global_level = self.level.unwrap_or(LevelFilter::Info);
1017        let root_level = self.root_level.unwrap_or(global_level);
1018        if global_level < root_level {
1019            warn!(
1020                "Logs with level more verbose than {} will be ignored",
1021                global_level,
1022            );
1023        }
1024
1025        let (sync_sender, receiver) = match &self.bounded_channel_option {
1026            None => unbounded(),
1027            Some(option) => bounded(option.size),
1028        };
1029        let (notification_sender, notification_receiver) = bounded(1);
1030        std::thread::Builder::new()
1031            .name("logger".to_string())
1032            .spawn(move || {
1033                let mut appenders = self.appenders;
1034                let filters = filters;
1035
1036                for filter in &filters {
1037                    if let Some(level) = filter.level {
1038                        if global_level < level {
1039                            warn!(
1040                                "Logs with level more verbose than {} will be ignored in `{}` ",
1041                                global_level, filter.path,
1042                            );
1043                        }
1044                    }
1045                }
1046
1047                let mut root = self.root;
1048                let mut last_log = HashMap::default();
1049                let mut missed_log = HashMap::default();
1050                let mut last_flush = Instant::now();
1051                let timeout = Duration::from_millis(200);
1052                loop {
1053                    match receiver.recv_timeout(timeout) {
1054                        Ok(LoggerInput::LogMsg(log_msg)) => {
1055                            log_msg.write(
1056                                &filters,
1057                                &mut appenders,
1058                                &mut root,
1059                                root_level,
1060                                &mut missed_log,
1061                                &mut last_log,
1062                                offset,
1063                                &time_format,
1064                            );
1065                        }
1066                        Ok(LoggerInput::Flush) => {
1067                            let max = receiver.len();
1068                            'queue: for _ in 1..=max {
1069                                if let Ok(LoggerInput::LogMsg(msg)) = receiver.try_recv() {
1070                                    msg.write(
1071                                        &filters,
1072                                        &mut appenders,
1073                                        &mut root,
1074                                        root_level,
1075                                        &mut missed_log,
1076                                        &mut last_log,
1077                                        offset,
1078                                        &time_format,
1079                                    )
1080                                } else {
1081                                    break 'queue;
1082                                }
1083                            }
1084                            let flush_result = appenders
1085                                .values_mut()
1086                                .chain([&mut root])
1087                                .find_map(|w| w.flush().err());
1088                            if let Some(error) = flush_result {
1089                                notification_sender
1090                                    .send(LoggerOutput::FlushError(error))
1091                                    .expect("logger notification failed");
1092                            } else {
1093                                notification_sender
1094                                    .send(LoggerOutput::Flushed)
1095                                    .expect("logger notification failed");
1096                            }
1097                        }
1098                        Err(RecvTimeoutError::Timeout) => {
1099                            if last_flush.elapsed() > Duration::from_millis(1000) {
1100                                let flush_errors = appenders
1101                                    .values_mut()
1102                                    .chain([&mut root])
1103                                    .filter_map(|w| w.flush().err());
1104                                for err in flush_errors {
1105                                    log::warn!("Ftlog flush error: {}", err);
1106                                }
1107                                last_flush = Instant::now();
1108                            };
1109                        }
1110                        Err(_) => break,
1111                    }
1112                }
1113            })?;
1114        let block = self
1115            .bounded_channel_option
1116            .as_ref()
1117            .map(|x| x.block)
1118            .unwrap_or(false);
1119        let print = self
1120            .bounded_channel_option
1121            .as_ref()
1122            .map(|x| x.print)
1123            .unwrap_or(false);
1124        Ok(Logger {
1125            format: self.format,
1126            level: global_level,
1127            queue: sync_sender,
1128            notification: notification_receiver,
1129            block,
1130            discard_state: if block || !print {
1131                None
1132            } else {
1133                Some(DiscardState {
1134                    last: ArcSwap::new(Arc::new(Instant::now())),
1135                    count: AtomicUsize::new(0),
1136                })
1137            },
1138            stopped: AtomicBool::new(false),
1139        })
1140    }
1141
1142    /// try building and setting as global logger
1143    pub fn try_init(self) -> Result<LoggerGuard, Box<dyn std::error::Error>> {
1144        let logger = self.build()?;
1145        Ok(logger.init()?)
1146    }
1147}
1148
1149impl Default for Builder {
1150    #[inline]
1151    fn default() -> Self {
1152        Builder::new()
1153    }
1154}