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    pub fn new<P1, P2>(dir: P1, file_name: P2, level: Level, format: LogFormat) -> Self
175    where
176        P1: Into<PathBuf>,
177        P2: Into<PathBuf>,
178    {
179        let dir_path: PathBuf = dir.into();
180        if !dir_path.exists() {
181            std::fs::create_dir(&dir_path).expect("create dir for log");
182        }
183        let file_path = dir_path.join(file_name.into()).into_boxed_path();
184        Self { level, format, file_path }
185    }
186}
187
188impl SinkConfigTrait for LogRawFile {
189    fn get_level(&self) -> Level {
190        self.level
191    }
192
193    fn get_file_path(&self) -> Option<Box<Path>> {
194        Some(self.file_path.clone())
195    }
196
197    fn write_hash(&self, hasher: &mut Box<dyn Hasher>) {
198        self.hash(hasher);
199        hasher.write(b"LogRawFile");
200    }
201
202    fn build(&self) -> LoggerSink {
203        LoggerSink::File(LoggerSinkFile::new(self))
204    }
205}
206
207#[derive(Copy, Clone, Debug, Hash, PartialEq)]
208#[repr(u8)]
209pub enum ConsoleTarget {
210    Stdout = 1,
211    Stderr = 2,
212}
213
214impl FromStr for ConsoleTarget {
215    type Err = ();
216
217    /// accepts case-insensitive: stdout, stderr, out, err, 1, 2
218    fn from_str(s: &str) -> Result<Self, ()> {
219        let v = s.to_lowercase();
220        match v.as_str() {
221            "stdout" => Ok(ConsoleTarget::Stdout),
222            "stderr" => Ok(ConsoleTarget::Stderr),
223            "out" => Ok(ConsoleTarget::Stdout),
224            "err" => Ok(ConsoleTarget::Stderr),
225            "1" => Ok(ConsoleTarget::Stdout),
226            "2" => Ok(ConsoleTarget::Stderr),
227            _ => Err(()),
228        }
229    }
230}
231
232#[derive(Hash)]
233pub struct LogConsole {
234    pub target: ConsoleTarget,
235
236    /// max log level in this file
237    pub level: Level,
238
239    pub format: LogFormat,
240}
241
242impl LogConsole {
243    pub fn new(target: ConsoleTarget, level: Level, format: LogFormat) -> Self {
244        Self { target, level, format }
245    }
246}
247
248impl SinkConfigTrait for LogConsole {
249    fn get_level(&self) -> Level {
250        self.level
251    }
252
253    fn get_file_path(&self) -> Option<Box<Path>> {
254        None
255    }
256
257    fn write_hash(&self, hasher: &mut Box<dyn Hasher>) {
258        self.hash(hasher);
259        hasher.write(b"LogConsole");
260    }
261
262    fn build(&self) -> LoggerSink {
263        LoggerSink::Console(LoggerSinkConsole::new(self))
264    }
265}
266
267pub struct EnvVarDefault<'a, T> {
268    name: &'a str,
269    default: T,
270}
271
272/// To config some logger setting with env.
273///
274/// Read value from environment, and set with default if not exists.
275///
276/// NOTE: the arguments to load from env_or() must support owned values.
277///
278/// Example:
279///
280/// ```rust
281/// use captains_log::*;
282/// let _level: log::Level = env_or("LOG_LEVEL", Level::Info).into();
283/// let _file_path: String = env_or("LOG_FILE", "/tmp/test.log").into();
284/// let _console: ConsoleTarget = env_or("LOG_CONSOLE", ConsoleTarget::Stdout).into();
285/// ```
286pub fn env_or<'a, T>(name: &'a str, default: T) -> EnvVarDefault<'a, T> {
287    EnvVarDefault { name, default }
288}
289
290impl<'a> Into<String> for EnvVarDefault<'a, &'a str> {
291    fn into(self) -> String {
292        if let Ok(v) = std::env::var(&self.name) {
293            return v;
294        }
295        return self.default.to_string();
296    }
297}
298
299impl<'a, P: AsRef<Path>> Into<PathBuf> for EnvVarDefault<'a, P> {
300    fn into(self) -> PathBuf {
301        if let Some(v) = std::env::var_os(&self.name) {
302            if v.len() > 0 {
303                return PathBuf::from(v);
304            }
305        }
306        return self.default.as_ref().to_path_buf();
307    }
308}
309
310macro_rules! impl_from_env {
311    ($type: tt) => {
312        impl<'a> Into<$type> for EnvVarDefault<'a, $type> {
313            #[inline]
314            fn into(self) -> $type {
315                if let Ok(v) = std::env::var(&self.name) {
316                    match $type::from_str(&v) {
317                        Ok(r) => return r,
318                        Err(_) => {
319                            eprintln!(
320                                "env {}={} is not valid, set to {:?}",
321                                self.name, v, self.default
322                            );
323                        }
324                    }
325                }
326                return self.default;
327            }
328        }
329    };
330}
331
332// Tried to impl blanket trait T: FromStr, rust reports conflict with
333// - impl<T, U> Into<U> for T where U: From<T>;
334impl_from_env!(ConsoleTarget);
335impl_from_env!(Level);
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::recipe;
341
342    #[test]
343    fn test_raw_file() {
344        let _file_sink = LogRawFile::new("/tmp", "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
345        let dir_path = Path::new("/tmp/test_dir");
346        if dir_path.is_dir() {
347            std::fs::remove_dir(&dir_path).expect("ok");
348        }
349        let _file_sink =
350            LogRawFile::new(&dir_path, "test.log", Level::Info, recipe::LOG_FORMAT_DEBUG);
351        assert!(dir_path.is_dir());
352        std::fs::remove_dir(&dir_path).expect("ok");
353    }
354
355    #[test]
356    fn test_env_config() {
357        // test log level
358        unsafe { std::env::set_var("LEVEL", "warn") };
359        let level: Level = env_or("LEVEL", Level::Debug).into();
360        assert_eq!(level, Level::Warn);
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
365        assert_eq!(ConsoleTarget::from_str("Stdout").unwrap(), ConsoleTarget::Stdout);
366        assert_eq!(ConsoleTarget::from_str("StdERR").unwrap(), ConsoleTarget::Stderr);
367        assert_eq!(ConsoleTarget::from_str("1").unwrap(), ConsoleTarget::Stdout);
368        assert_eq!(ConsoleTarget::from_str("2").unwrap(), ConsoleTarget::Stderr);
369        assert_eq!(ConsoleTarget::from_str("0").unwrap_err(), ());
370
371        // test console target
372        unsafe { std::env::set_var("CONSOLE", "stderr") };
373        let target: ConsoleTarget = env_or("CONSOLE", ConsoleTarget::Stdout).into();
374        assert_eq!(target, ConsoleTarget::Stderr);
375        unsafe { std::env::set_var("CONSOLE", "") };
376        let target: ConsoleTarget = env_or("CONSOLE", ConsoleTarget::Stdout).into();
377        assert_eq!(target, ConsoleTarget::Stdout);
378
379        // test path
380        unsafe { std::env::set_var("LOG_PATH", "/tmp/test.log") };
381        let path: PathBuf = env_or("LOG_PATH", "/tmp/other.log").into();
382        assert_eq!(path, Path::new("/tmp/test.log").to_path_buf());
383
384        unsafe { std::env::set_var("LOG_PATH", "") };
385        let path: PathBuf = env_or("LOG_PATH", "/tmp/other.log").into();
386        assert_eq!(path, Path::new("/tmp/other.log").to_path_buf());
387
388        let _builder = recipe::raw_file_logger(env_or("LOG_PATH", "/tmp/other.log"), Level::Info);
389    }
390}