captains_log/
config.rs

1use crate::log_impl::setup_log;
2use crate::{
3    console_impl::LoggerSinkConsole,
4    file_impl::LoggerSinkFile,
5    formatter::{FormatRecord, TimeFormatter},
6    log_impl::LoggerSink,
7    time::Timer,
8};
9use log::{Level, LevelFilter, Record};
10use std::hash::{DefaultHasher, Hash, Hasher};
11use std::path::Path;
12
13/// Global config to setup logger
14/// See crate::recipe for usage
15#[derive(Default)]
16pub struct Builder {
17    /// When dynamic==true,
18    ///   Can safely re-initialize GlobalLogger even it exists,
19    ///   useful to setup different types of logger in test suits.
20    /// When dynamic==false,
21    ///   Only initialize once, logger sinks setting cannot be change afterwards.
22    ///   More efficient for production environment.
23    pub dynamic: bool,
24
25    /// Listen for signal of log-rotate
26    /// NOTE: Once logger started to listen signal, does not support dynamic reconfigure.
27    pub rotation_signals: Vec<i32>,
28
29    /// Hookup to log error when panic
30    pub panic: bool,
31
32    /// Whether to exit program after panic
33    pub continue_when_panic: bool,
34
35    /// Different types of log sink
36    pub sinks: Vec<Box<dyn SinkConfigTrait>>,
37}
38
39impl Builder {
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// For test cases, set dynamic=true and turn Off signal.
45    /// Call this with pre-set recipe for convenient.
46    pub fn test(mut self) -> Self {
47        self.dynamic = true;
48        self.rotation_signals.clear();
49        self
50    }
51
52    /// Add log-rotate signal
53    pub fn signal(mut self, signal: i32) -> Self {
54        self.rotation_signals.push(signal);
55        self
56    }
57
58    /// Add raw file sink that supports multiprocess atomic append
59    pub fn raw_file(mut self, config: LogRawFile) -> Self {
60        self.sinks.push(Box::new(config));
61        self
62    }
63
64    /// Add console sink
65    pub fn console(mut self, config: LogConsole) -> Self {
66        self.sinks.push(Box::new(config));
67        self
68    }
69
70    /// Return the max log level in the log sinks
71    pub fn get_max_level(&self) -> LevelFilter {
72        let mut max_level = Level::Error;
73        for sink in &self.sinks {
74            let level = sink.get_level();
75            if level > max_level {
76                max_level = level;
77            }
78        }
79        return max_level.to_level_filter();
80    }
81
82    /// Calculate checksum of the setting for init() comparison
83    pub(crate) fn cal_checksum(&self) -> u64 {
84        let mut hasher = Box::new(DefaultHasher::new()) as Box<dyn Hasher>;
85        self.dynamic.hash(&mut hasher);
86        self.rotation_signals.hash(&mut hasher);
87        self.panic.hash(&mut hasher);
88        self.continue_when_panic.hash(&mut hasher);
89        for sink in &self.sinks {
90            sink.write_hash(&mut hasher);
91        }
92        hasher.finish()
93    }
94
95    /// Setup global logger.
96    /// Equals to setup_log(builder)
97    pub fn build(self) -> Result<(), ()> {
98        setup_log(self)
99    }
100}
101
102pub trait SinkConfigTrait {
103    /// get max log level of the sink
104    fn get_level(&self) -> Level;
105    /// Only file sink has path
106    fn get_file_path(&self) -> Option<Box<Path>>;
107    /// Calculate hash for config comparison
108    fn write_hash(&self, hasher: &mut Box<dyn Hasher>);
109    /// Build an actual sink from config
110    fn build(&self) -> LoggerSink;
111}
112
113pub type FormatFunc = fn(FormatRecord) -> String;
114
115/// Custom formatter which adds into a log sink
116#[derive(Clone, Hash)]
117pub struct LogFormat {
118    time_fmt: String,
119    format_fn: FormatFunc,
120}
121
122impl LogFormat {
123    /// # Arguments
124    ///
125    /// time_fmt: refer to chrono::format::strftime.
126    ///
127    /// format_fn:
128    /// Since std::fmt only support compile time format,
129    /// you have to write a static function to format the log line
130    ///
131    /// # Example
132    /// ```
133    /// use captains_log::{LogRawFile, LogFormat, FormatRecord};
134    /// fn format_f(r: FormatRecord) -> String {
135    ///     let time = r.time();
136    ///     let level = r.level();
137    ///     let msg = r.msg();
138    ///     let req_id = r.key("req_id");
139    ///     format!("[{time}][{level}] {msg}{req_id}\n").to_string()
140    /// }
141    /// let log_format = LogFormat::new("%Y-%m-%d %H:%M:%S%.6f", format_f);
142    /// let log_sink = LogRawFile::new("/tmp", "test.log", log::Level::Info, log_format);
143    /// ```
144
145    pub fn new(time_fmt: &str, format_fn: FormatFunc) -> Self {
146        Self { time_fmt: time_fmt.to_string(), format_fn }
147    }
148
149    #[inline(always)]
150    pub(crate) fn process(&self, now: &Timer, record: &Record) -> String {
151        let time = TimeFormatter { now, fmt_str: &self.time_fmt };
152        let r = FormatRecord { record, time };
153        return (self.format_fn)(r);
154    }
155}
156
157/// Config for file sink that supports atomic append from multiprocess.
158/// For log rotation, you need system log-rotate service to notify with signal.
159#[derive(Hash)]
160pub struct LogRawFile {
161    /// Directory path
162    pub dir: String,
163
164    /// max log level in this file
165    pub level: Level,
166
167    /// filename
168    pub name: String,
169
170    pub format: LogFormat,
171
172    /// path: dir/name
173    pub file_path: Box<Path>,
174}
175
176impl LogRawFile {
177    pub fn new(dir: &str, name: &str, level: Level, format: LogFormat) -> Self {
178        let file_path = Path::new(dir).join(Path::new(name)).into_boxed_path();
179        Self { dir: dir.to_string(), name: name.to_string(), level, format, file_path }
180    }
181}
182
183impl SinkConfigTrait for LogRawFile {
184    fn get_level(&self) -> Level {
185        self.level
186    }
187
188    fn get_file_path(&self) -> Option<Box<Path>> {
189        Some(self.file_path.clone())
190    }
191
192    fn write_hash(&self, hasher: &mut Box<dyn Hasher>) {
193        self.hash(hasher);
194        hasher.write(b"LogRawFile");
195    }
196
197    fn build(&self) -> LoggerSink {
198        LoggerSink::File(LoggerSinkFile::new(self))
199    }
200}
201
202#[derive(Copy, Clone, Debug, Hash)]
203#[repr(u8)]
204pub enum ConsoleTarget {
205    Stdout = 1,
206    Stderr = 2,
207}
208
209#[derive(Hash)]
210pub struct LogConsole {
211    pub target: ConsoleTarget,
212
213    /// max log level in this file
214    pub level: Level,
215
216    pub format: LogFormat,
217}
218
219impl LogConsole {
220    pub fn new(target: ConsoleTarget, level: Level, format: LogFormat) -> Self {
221        Self { target, level, format }
222    }
223}
224
225impl SinkConfigTrait for LogConsole {
226    fn get_level(&self) -> Level {
227        self.level
228    }
229
230    fn get_file_path(&self) -> Option<Box<Path>> {
231        None
232    }
233
234    fn write_hash(&self, hasher: &mut Box<dyn Hasher>) {
235        self.hash(hasher);
236        hasher.write(b"LogConsole");
237    }
238
239    fn build(&self) -> LoggerSink {
240        LoggerSink::Console(LoggerSinkConsole::new(self))
241    }
242}