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}