ftlog/lib.rs
1//! [](https://github.com/nonconvextech/ftlog/actions)
2//! 
3//! [](https://crates.io/crates/ftlog)
4//! [](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}