Skip to main content

captains_log/
config.rs

1use crate::log_impl::setup_log;
2use crate::{
3    formatter::{FormatRecord, TimeFormatter},
4    log_impl::{GlobalLogger, LogSink, LogSinkTrait},
5    time::Timer,
6};
7use log::{Level, LevelFilter, Record};
8use std::hash::{DefaultHasher, Hash, Hasher};
9use std::path::Path;
10
11/// Global config to setup logger
12/// See crate::recipe for usage
13#[derive(Default)]
14pub struct Builder {
15    /// When dynamic==true,
16    ///   Can safely re-initialize GlobalLogger even it exists,
17    ///   useful to setup different types of logger in test suits.
18    /// When dynamic==false,
19    ///   Only initialize once, logger sinks setting cannot be change afterwards.
20    ///   More efficient for production environment.
21    pub dynamic: bool,
22
23    /// Listen for signal of log-rotate
24    /// NOTE: Once logger started to listen signal, does not support dynamic reconfigure.
25    pub rotation_signals: Vec<i32>,
26
27    /// default to true we will hookup to log error when panic, but panic will go on (which works
28    /// with test cases has `should_panic`)
29    pub panic_hook: bool,
30
31    /// By default this is false,
32    /// and panic hook will not consume the error (which works with should_panic).
33    /// If set to true, it will force a double panic which cannot be intercept by catch_unwind.
34    /// It's useful to force abort on panic under tokio spawn tasks.
35    ///
36    /// Note that there's only one effective panic hook by default. The later once will override
37    /// those set previously.
38    pub force_abort_on_panic: bool,
39
40    /// Different types of log sink
41    pub sinks: Vec<Box<dyn SinkConfigTrait>>,
42
43    /// subscribe to tracing as global dispatcher
44    #[cfg(feature = "tracing")]
45    #[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
46    pub tracing_global: bool,
47}
48
49impl Builder {
50    pub fn new() -> Self {
51        Self { panic_hook: true, ..Default::default() }
52    }
53
54    /// subscribe to tracing as global dispatcher
55    #[cfg(feature = "tracing")]
56    #[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
57    #[inline]
58    pub fn tracing_global(mut self) -> Self {
59        self.tracing_global = true;
60        self
61    }
62
63    /// do not log error on panic
64    pub fn no_panic_hook(mut self) -> Self {
65        self.panic_hook = false;
66        self
67    }
68
69    /// By default this is false, and panic hook will not consume the error (which works with should_panic).
70    /// If `force_abort_on_panic` set to true, it will force a double panic which cannot be intercept by catch_unwind.
71    /// It's useful to force abort on panic under tokio spawn tasks.
72    ///
73    /// # NOTE
74    ///
75    /// this does not compatible with `should_panic` test cases.
76    ///
77    /// there's only one effective panic hook by default. The later once will override
78    /// those set previously.
79    pub fn force_abort_on_panic(mut self) -> Self {
80        self.force_abort_on_panic = true;
81        self
82    }
83
84    /// For test cases, set dynamic=true and turn Off signal.
85    /// Call this with pre-set recipe for convenient.
86    #[inline]
87    pub fn test(mut self) -> Self {
88        self.dynamic = true;
89        self.rotation_signals.clear();
90        self
91    }
92
93    /// Add log-rotate signal
94    #[inline]
95    pub fn signal(mut self, signal: i32) -> Self {
96        self.rotation_signals.push(signal);
97        self
98    }
99
100    /// Add different types of log sink config, can be called multiple times.
101    #[inline]
102    pub fn add_sink<S: SinkConfigTrait>(mut self, config: S) -> Self {
103        self.sinks.push(Box::new(config));
104        self
105    }
106
107    /// Return the max log level in the log sinks
108    #[inline]
109    pub fn get_max_level(&self) -> LevelFilter {
110        let mut max_level = Level::Error;
111        for sink in &self.sinks {
112            let level = sink.get_level();
113            if level > max_level {
114                max_level = level;
115            }
116        }
117        return max_level.to_level_filter();
118    }
119
120    /// Calculate checksum of the setting for init() comparison
121    #[inline]
122    pub(crate) fn cal_checksum(&self) -> u64 {
123        let mut hasher = Box::new(DefaultHasher::new()) as Box<dyn Hasher>;
124        self.dynamic.hash(&mut hasher);
125        self.rotation_signals.hash(&mut hasher);
126        self.panic_hook.hash(&mut hasher);
127        self.force_abort_on_panic.hash(&mut hasher);
128        for sink in &self.sinks {
129            sink.write_hash(&mut hasher);
130        }
131        hasher.finish()
132    }
133
134    #[inline]
135    pub(crate) fn build_sinks(&self) -> std::io::Result<Vec<LogSink>> {
136        let mut sinks = Vec::new();
137        for config in &self.sinks {
138            let logger_sink = config.build();
139            if let Err(e) = logger_sink.open() {
140                eprintln!("failed to open log sink: {:?}", e);
141                return Err(e);
142            }
143            sinks.push(logger_sink);
144        }
145        Ok(sinks)
146    }
147
148    /// Setup global logger.
149    /// Equals to setup_log(builder)
150    ///
151    /// **NOTE**: You can call this function multiple times when **builder.dynamic=true**,
152    /// but **cannot mixed used captains_log with other logger implement**, because log::set_logger()
153    /// cannot be called twice.
154    pub fn build(self) -> std::io::Result<&'static GlobalLogger> {
155        setup_log(self)
156    }
157}
158
159pub(crate) trait SinkConfigBuild {
160    /// Build an actual sink from config
161    fn build(&self) -> LogSink;
162}
163
164#[allow(private_bounds)]
165pub trait SinkConfigTrait: 'static + SinkConfigBuild {
166    /// get max log level of the sink
167    fn get_level(&self) -> Level;
168    /// Only file sink has path
169    #[allow(dead_code)]
170    fn get_file_path(&self) -> Option<Box<Path>>;
171    /// Calculate hash for config comparison
172    fn write_hash(&self, hasher: &mut Box<dyn Hasher>);
173}
174
175pub type FormatFunc = fn(FormatRecord) -> String;
176
177/// Custom formatter which adds into a log sink
178#[derive(Clone, Hash)]
179pub struct LogFormat {
180    time_fmt: &'static str,
181    format_fn: FormatFunc,
182}
183
184impl LogFormat {
185    /// # Arguments
186    ///
187    /// time_fmt: refer to chrono::format::strftime.
188    ///
189    /// format_fn:
190    /// Since std::fmt only support compile time format,
191    /// you have to write a static function to format the log line
192    ///
193    /// # Example
194    /// ```
195    /// use captains_log::{LogRawFile, LogFormat, FormatRecord};
196    /// fn format_f(r: FormatRecord) -> String {
197    ///     let time = r.time();
198    ///     let level = r.level();
199    ///     let msg = r.msg();
200    ///     let req_id = r.key("req_id");
201    ///     format!("[{time}][{level}] {msg}{req_id}\n").to_string()
202    /// }
203    /// let log_format = LogFormat::new("%Y-%m-%d %H:%M:%S%.6f", format_f);
204    /// let log_sink = LogRawFile::new("/tmp", "test.log", log::Level::Info, log_format);
205    /// ```
206    pub const fn new(time_fmt: &'static str, format_fn: FormatFunc) -> Self {
207        Self { time_fmt, format_fn }
208    }
209
210    #[inline(always)]
211    pub(crate) fn process(&self, now: &Timer, record: &Record) -> String {
212        let time = TimeFormatter { now, fmt_str: self.time_fmt };
213        let r = FormatRecord { record, time };
214        return (self.format_fn)(r);
215    }
216}