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, PathBuf};
12use std::str::FromStr;
13
14/// Global config to setup logger
15/// See crate::recipe for usage
16#[derive(Default)]
17pub struct Builder {
18    /// When dynamic==true,
19    ///   Can safely re-initialize GlobalLogger even it exists,
20    ///   useful to setup different types of logger in test suits.
21    /// When dynamic==false,
22    ///   Only initialize once, logger sinks setting cannot be change afterwards.
23    ///   More efficient for production environment.
24    pub dynamic: bool,
25
26    /// Listen for signal of log-rotate
27    /// NOTE: Once logger started to listen signal, does not support dynamic reconfigure.
28    pub rotation_signals: Vec<i32>,
29
30    /// Hookup to log error when panic
31    pub panic: bool,
32
33    /// Whether to exit program after panic
34    pub continue_when_panic: bool,
35
36    /// Different types of log sink
37    pub sinks: Vec<Box<dyn SinkConfigTrait>>,
38}
39
40impl Builder {
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// For test cases, set dynamic=true and turn Off signal.
46    /// Call this with pre-set recipe for convenient.
47    pub fn test(mut self) -> Self {
48        self.dynamic = true;
49        self.rotation_signals.clear();
50        self
51    }
52
53    /// Add log-rotate signal
54    pub fn signal(mut self, signal: i32) -> Self {
55        self.rotation_signals.push(signal);
56        self
57    }
58
59    /// Add raw file sink that supports multiprocess atomic append
60    pub fn raw_file(mut self, config: LogRawFile) -> Self {
61        self.sinks.push(Box::new(config));
62        self
63    }
64
65    /// Add console sink
66    pub fn console(mut self, config: LogConsole) -> Self {
67        self.sinks.push(Box::new(config));
68        self
69    }
70
71    /// Return the max log level in the log sinks
72    pub fn get_max_level(&self) -> LevelFilter {
73        let mut max_level = Level::Error;
74        for sink in &self.sinks {
75            let level = sink.get_level();
76            if level > max_level {
77                max_level = level;
78            }
79        }
80        return max_level.to_level_filter();
81    }
82
83    /// Calculate checksum of the setting for init() comparison
84    pub(crate) fn cal_checksum(&self) -> u64 {
85        let mut hasher = Box::new(DefaultHasher::new()) as Box<dyn Hasher>;
86        self.dynamic.hash(&mut hasher);
87        self.rotation_signals.hash(&mut hasher);
88        self.panic.hash(&mut hasher);
89        self.continue_when_panic.hash(&mut hasher);
90        for sink in &self.sinks {
91            sink.write_hash(&mut hasher);
92        }
93        hasher.finish()
94    }
95
96    /// Setup global logger.
97    /// Equals to setup_log(builder)
98    pub fn build(self) -> Result<(), ()> {
99        setup_log(self)
100    }
101}
102
103pub trait SinkConfigTrait {
104    /// get max log level of the sink
105    fn get_level(&self) -> Level;
106    /// Only file sink has path
107    fn get_file_path(&self) -> Option<Box<Path>>;
108    /// Calculate hash for config comparison
109    fn write_hash(&self, hasher: &mut Box<dyn Hasher>);
110    /// Build an actual sink from config
111    fn build(&self) -> LoggerSink;
112}
113
114pub type FormatFunc = fn(FormatRecord) -> String;
115
116/// Custom formatter which adds into a log sink
117#[derive(Clone, Hash)]
118pub struct LogFormat {
119    time_fmt: &'static str,
120    format_fn: FormatFunc,
121}
122
123impl LogFormat {
124    /// # Arguments
125    ///
126    /// time_fmt: refer to chrono::format::strftime.
127    ///
128    /// format_fn:
129    /// Since std::fmt only support compile time format,
130    /// you have to write a static function to format the log line
131    ///
132    /// # Example
133    /// ```
134    /// use captains_log::{LogRawFile, LogFormat, FormatRecord};
135    /// fn format_f(r: FormatRecord) -> String {
136    ///     let time = r.time();
137    ///     let level = r.level();
138    ///     let msg = r.msg();
139    ///     let req_id = r.key("req_id");
140    ///     format!("[{time}][{level}] {msg}{req_id}\n").to_string()
141    /// }
142    /// let log_format = LogFormat::new("%Y-%m-%d %H:%M:%S%.6f", format_f);
143    /// let log_sink = LogRawFile::new("/tmp", "test.log", log::Level::Info, log_format);
144    /// ```
145
146    pub const fn new(time_fmt: &'static str, format_fn: FormatFunc) -> Self {
147        Self { time_fmt, format_fn }
148    }
149
150    #[inline(always)]
151    pub(crate) fn process(&self, now: &Timer, record: &Record) -> String {
152        let time = TimeFormatter { now, fmt_str: self.time_fmt };
153        let r = FormatRecord { record, time };
154        return (self.format_fn)(r);
155    }
156}
157
158/// Config for file sink that supports atomic append from multiprocess.
159/// For log rotation, you need system log-rotate service to notify with signal.
160#[derive(Hash)]
161pub struct LogRawFile {
162    /// max log level in this file
163    pub level: Level,
164
165    pub format: LogFormat,
166
167    /// path: dir/name
168    pub file_path: Box<Path>,
169}
170
171impl LogRawFile {
172    /// Construct config for file sink,
173    /// will try to create dir if not exists.
174    ///
175    /// The type of `dir` and `file_name` can be &str / String / &OsStr / OsString / Path / PathBuf. They can be of
176    /// different types.
177    pub fn new<P1, P2>(dir: P1, file_name: P2, level: Level, format: LogFormat) -> Self
178    where
179        P1: Into<PathBuf>,
180        P2: Into<PathBuf>,
181    {
182        let dir_path: PathBuf = dir.into();
183        if !dir_path.exists() {
184            std::fs::create_dir(&dir_path).expect("create dir for log");
185        }
186        let file_path = dir_path.join(file_name.into()).into_boxed_path();
187        Self { level, format, file_path }
188    }
189}
190
191impl SinkConfigTrait for LogRawFile {
192    fn get_level(&self) -> Level {
193        self.level
194    }
195
196    fn get_file_path(&self) -> Option<Box<Path>> {
197        Some(self.file_path.clone())
198    }
199
200    fn write_hash(&self, hasher: &mut Box<dyn Hasher>) {
201        self.hash(hasher);
202        hasher.write(b"LogRawFile");
203    }
204
205    fn build(&self) -> LoggerSink {
206        LoggerSink::File(LoggerSinkFile::new(self))
207    }
208}
209
210#[derive(Copy, Clone, Debug, Hash, PartialEq)]
211#[repr(u8)]
212pub enum ConsoleTarget {
213    Stdout = 1,
214    Stderr = 2,
215}
216
217impl FromStr for ConsoleTarget {
218    type Err = ();
219
220    /// accepts case-insensitive: stdout, stderr, out, err, 1, 2
221    fn from_str(s: &str) -> Result<Self, ()> {
222        let v = s.to_lowercase();
223        match v.as_str() {
224            "stdout" => Ok(ConsoleTarget::Stdout),
225            "stderr" => Ok(ConsoleTarget::Stderr),
226            "out" => Ok(ConsoleTarget::Stdout),
227            "err" => Ok(ConsoleTarget::Stderr),
228            "1" => Ok(ConsoleTarget::Stdout),
229            "2" => Ok(ConsoleTarget::Stderr),
230            _ => Err(()),
231        }
232    }
233}
234
235#[derive(Hash)]
236pub struct LogConsole {
237    pub target: ConsoleTarget,
238
239    /// max log level in this file
240    pub level: Level,
241
242    pub format: LogFormat,
243}
244
245impl LogConsole {
246    pub fn new(target: ConsoleTarget, level: Level, format: LogFormat) -> Self {
247        Self { target, level, format }
248    }
249}
250
251impl SinkConfigTrait for LogConsole {
252    fn get_level(&self) -> Level {
253        self.level
254    }
255
256    fn get_file_path(&self) -> Option<Box<Path>> {
257        None
258    }
259
260    fn write_hash(&self, hasher: &mut Box<dyn Hasher>) {
261        self.hash(hasher);
262        hasher.write(b"LogConsole");
263    }
264
265    fn build(&self) -> LoggerSink {
266        LoggerSink::Console(LoggerSinkConsole::new(self))
267    }
268}
269
270pub struct EnvVarDefault<'a, T> {
271    name: &'a str,
272    default: T,
273}
274
275/// To config some logger setting with env.
276///
277/// Read value from environment, and set with default if not exists.
278///
279/// NOTE: the arguments to load from env_or() must support owned values.
280///
281/// Example:
282///
283/// ```rust
284/// use captains_log::*;
285/// let _level: log::Level = env_or("LOG_LEVEL", Level::Info).into();
286/// let _file_path: String = env_or("LOG_FILE", "/tmp/test.log").into();
287/// let _console: ConsoleTarget = env_or("LOG_CONSOLE", ConsoleTarget::Stdout).into();
288/// ```
289pub fn env_or<'a, T>(name: &'a str, default: T) -> EnvVarDefault<'a, T> {
290    EnvVarDefault { name, default }
291}
292
293impl<'a> Into<String> for EnvVarDefault<'a, &'a str> {
294    fn into(self) -> String {
295        if let Ok(v) = std::env::var(&self.name) {
296            return v;
297        }
298        return self.default.to_string();
299    }
300}
301
302impl<'a, P: AsRef<Path>> Into<PathBuf> for EnvVarDefault<'a, P> {
303    fn into(self) -> PathBuf {
304        if let Some(v) = std::env::var_os(&self.name) {
305            if v.len() > 0 {
306                return PathBuf::from(v);
307            }
308        }
309        return self.default.as_ref().to_path_buf();
310    }
311}
312
313macro_rules! impl_from_env {
314    ($type: tt) => {
315        impl<'a> Into<$type> for EnvVarDefault<'a, $type> {
316            #[inline]
317            fn into(self) -> $type {
318                if let Ok(v) = std::env::var(&self.name) {
319                    match $type::from_str(&v) {
320                        Ok(r) => return r,
321                        Err(_) => {
322                            eprintln!(
323                                "env {}={} is not valid, set to {:?}",
324                                self.name, v, self.default
325                            );
326                        }
327                    }
328                }
329                return self.default;
330            }
331        }
332    };
333}
334
335// Tried to impl blanket trait T: FromStr, rust reports conflict with
336// - impl<T, U> Into<U> for T where U: From<T>;
337impl_from_env!(ConsoleTarget);
338impl_from_env!(Level);
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use crate::recipe;
344
345    #[test]
346    fn test_raw_file() {
347        let _file_sink = LogRawFile::new("/tmp", "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
348        let dir_path = Path::new("/tmp/test_dir");
349        if dir_path.is_dir() {
350            std::fs::remove_dir(&dir_path).expect("ok");
351        }
352        let _file_sink =
353            LogRawFile::new(&dir_path, "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
354        assert!(dir_path.is_dir());
355        std::fs::remove_dir(&dir_path).expect("ok");
356    }
357
358    #[test]
359    fn test_env_config() {
360        // test log level
361        unsafe { std::env::set_var("LEVEL", "warn") };
362        let level: Level = env_or("LEVEL", Level::Debug).into();
363        assert_eq!(level, Level::Warn);
364        unsafe { std::env::set_var("LEVEL", "WARN") };
365        let level: Level = env_or("LEVEL", Level::Debug).into();
366        assert_eq!(level, Level::Warn);
367
368        assert_eq!(ConsoleTarget::from_str("Stdout").unwrap(), ConsoleTarget::Stdout);
369        assert_eq!(ConsoleTarget::from_str("StdERR").unwrap(), ConsoleTarget::Stderr);
370        assert_eq!(ConsoleTarget::from_str("1").unwrap(), ConsoleTarget::Stdout);
371        assert_eq!(ConsoleTarget::from_str("2").unwrap(), ConsoleTarget::Stderr);
372        assert_eq!(ConsoleTarget::from_str("0").unwrap_err(), ());
373
374        // test console target
375        unsafe { std::env::set_var("CONSOLE", "stderr") };
376        let target: ConsoleTarget = env_or("CONSOLE", ConsoleTarget::Stdout).into();
377        assert_eq!(target, ConsoleTarget::Stderr);
378        unsafe { std::env::set_var("CONSOLE", "") };
379        let target: ConsoleTarget = env_or("CONSOLE", ConsoleTarget::Stdout).into();
380        assert_eq!(target, ConsoleTarget::Stdout);
381
382        // test path
383        unsafe { std::env::set_var("LOG_PATH", "/tmp/test.log") };
384        let path: PathBuf = env_or("LOG_PATH", "/tmp/other.log").into();
385        assert_eq!(path, Path::new("/tmp/test.log").to_path_buf());
386
387        unsafe { std::env::set_var("LOG_PATH", "") };
388        let path: PathBuf = env_or("LOG_PATH", "/tmp/other.log").into();
389        assert_eq!(path, Path::new("/tmp/other.log").to_path_buf());
390
391        let _builder = recipe::raw_file_logger(env_or("LOG_PATH", "/tmp/other.log"), Level::Info);
392        let _builder =
393            recipe::raw_file_logger(env_or("LOG_PATH", "/tmp/other.log".to_string()), Level::Info);
394    }
395}