captains_log/
filter.rs

1//! # Fine-grain log filtering
2//!
3//! A large application may designed with multiple layers. Sometimes you have many files and modules,
4//! and you want more fine-grain controlling for the log, turn on / off by functionality.
5//!
6//! In order not limit by the number of log level, you can separate `LogFilter` into
7//! category, and place [LogFilter] in Arc and share among threads and coroutines.
8//! It will become more flexible with the number of `LogFilter` X log_level.
9//!
10//! When you want to debug the behavior on-the-flay,
11//! you can just change log level of a certain `LogFilter` with API.
12//!
13//! See the doc of [LogFilter] for details.
14//!
15//! In order For API level tracking, we provide `KeyFilter`, which inherits from `LogFilter`,
16//! a custom key can be placed in it. It's like human readable log with structure message.
17//! So that you can grep the log with specified request.
18//!
19//! See the doc of [KeyFilter] for details.
20
21use std::{
22    fmt,
23    ops::Deref,
24    str,
25    sync::atomic::{AtomicUsize, Ordering},
26};
27
28use log::{kv::*, *};
29
30pub trait Filter: Send + Sized + 'static {
31    /// whether a log level is enable
32    fn is_enabled(&self, _level: Level) -> bool;
33
34    /// for macros logger_XXX
35    #[doc(hidden)]
36    #[inline(always)]
37    fn _private_api_log(
38        &self, args: fmt::Arguments, level: Level,
39        &(target, module_path, file, line): &(&str, &str, &str, u32),
40    ) {
41        let record = RecordBuilder::new()
42            .level(level)
43            .target(target)
44            .module_path(Some(module_path))
45            .file(Some(file))
46            .line(Some(line))
47            .args(args)
48            .build();
49        logger().log(&record);
50    }
51}
52
53/// `LogFilter` supports concurrent control the log level filter with atomic.
54///
55/// Used in combine with macros logger_XXX. the log level filter can be dynamic changed.
56///
57/// # Example
58///
59/// ``` rust
60/// use std::sync::Arc;
61/// use captains_log::{*, filter::LogFilter};
62/// log::set_max_level(log::LevelFilter::Debug);
63/// let logger_io = Arc::new(LogFilter::new());
64/// let logger_req = Arc::new(LogFilter::new());
65/// logger_io.set_level(log::Level::Error);
66/// logger_req.set_level(log::Level::Debug);
67/// logger_debug!(logger_req, "Begin handle req ...");
68/// logger_debug!(logger_io, "Issue io to disk ...");
69/// logger_error!(logger_req, "Req invalid ...");
70/// ```
71pub struct LogFilter {
72    max_level: AtomicUsize,
73}
74
75impl Clone for LogFilter {
76    fn clone(&self) -> Self {
77        Self { max_level: AtomicUsize::new(self.get_level()) }
78    }
79}
80
81impl LogFilter {
82    pub fn new() -> Self {
83        Self { max_level: AtomicUsize::new(Level::Trace as usize) }
84    }
85
86    /// When LogFilter is shared in Arc, allows concurrently changing log level filter
87    #[inline]
88    pub fn set_level(&self, level: Level) {
89        self.max_level.store(level as usize, Ordering::Relaxed);
90    }
91
92    #[inline]
93    pub fn get_level(&self) -> usize {
94        self.max_level.load(Ordering::Relaxed)
95    }
96}
97
98impl Filter for LogFilter {
99    #[inline(always)]
100    fn is_enabled(&self, level: Level) -> bool {
101        level as usize <= self.max_level.load(Ordering::Relaxed)
102    }
103}
104
105impl log::kv::Source for LogFilter {
106    #[inline(always)]
107    fn visit<'kvs>(&'kvs self, _visitor: &mut dyn Visitor<'kvs>) -> Result<(), Error> {
108        Ok(())
109    }
110
111    #[inline(always)]
112    fn get<'a>(&'a self, _key: Key) -> Option<Value<'a>> {
113        return None;
114    }
115
116    #[inline(always)]
117    fn count(&self) -> usize {
118        0
119    }
120}
121
122/// A Filter that enables all log levels
123pub struct DummyFilter();
124
125impl Filter for DummyFilter {
126    #[inline(always)]
127    fn is_enabled(&self, _level: Level) -> bool {
128        true
129    }
130}
131
132impl log::kv::Source for DummyFilter {
133    #[inline(always)]
134    fn visit<'kvs>(&'kvs self, _visitor: &mut dyn Visitor<'kvs>) -> Result<(), Error> {
135        Ok(())
136    }
137
138    #[inline(always)]
139    fn get<'a>(&'a self, _key: Key) -> Option<Value<'a>> {
140        return None;
141    }
142
143    #[inline(always)]
144    fn count(&self) -> usize {
145        0
146    }
147}
148
149/// `KeyFilter` is inherited from [LogFilter], with one additional key into log format.
150///
151/// The name of the key can be customized.
152///
153/// Example for an http service, api handling log will have a field `req_id`.
154/// When you received error from one of the request,
155/// you can grep all the relevant log with that `req_id`.
156///
157/// ``` rust
158/// use captains_log::{*, filter::KeyFilter};
159/// fn debug_format_req_id_f(r: FormatRecord) -> String {
160///     let time = r.time();
161///     let level = r.level();
162///     let file = r.file();
163///     let line = r.line();
164///     let msg = r.msg();
165///     let req_id = r.key("req_id");
166///     format!("[{time}][{level}][{file}:{line}] {msg}{req_id}\n").to_string()
167/// }
168/// let builder = recipe::raw_file_logger_custom(
169///                 "/tmp/log_filter.log", log::Level::Debug,
170///                 recipe::DEFAULT_TIME, debug_format_req_id_f)
171///     .build().expect("setup log");
172///
173/// let logger = KeyFilter::new("req_id", format!("{:016x}", 123).to_string());
174/// info!("API service started");
175/// logger_debug!(logger, "Req / received");
176/// logger_debug!(logger, "header xxx");
177/// logger_info!(logger, "Req / 200 complete");
178/// ```
179///
180/// The log will be:
181///
182/// ``` text
183/// [2025-06-11 14:33:08.089090][DEBUG][request.rs:67] API service started
184/// [2025-06-11 14:33:10.099092][DEBUG][request.rs:67] Req / received (000000000000007b)
185/// [2025-06-11 14:33:10.099232][WARN][request.rs:68] header xxx (000000000000007b)
186/// [2025-06-11 14:33:11.009092][DEBUG][request.rs:67] Req / 200 complete (000000000000007b)
187/// ```
188#[derive(Clone)]
189pub struct KeyFilter {
190    inner: LogFilter,
191    key: &'static str,
192    value: String,
193}
194
195impl KeyFilter {
196    pub fn new(key: &'static str, value: String) -> Self {
197        Self { inner: LogFilter::new(), key, value }
198    }
199}
200
201impl log::kv::Source for KeyFilter {
202    #[inline(always)]
203    fn visit<'kvs>(&'kvs self, visitor: &mut dyn Visitor<'kvs>) -> Result<(), Error> {
204        visitor.visit_pair(self.key.to_key(), self.value.as_str().into())
205    }
206
207    #[inline(always)]
208    fn get<'a>(&'a self, key: Key) -> Option<Value<'a>> {
209        if key.as_ref() == self.key {
210            return Some(self.value.as_str().into());
211        }
212        return None;
213    }
214
215    #[inline(always)]
216    fn count(&self) -> usize {
217        1
218    }
219}
220
221impl Deref for KeyFilter {
222    type Target = LogFilter;
223
224    fn deref(&self) -> &Self::Target {
225        &self.inner
226    }
227}
228
229impl Filter for KeyFilter {
230    #[inline(always)]
231    fn is_enabled(&self, level: Level) -> bool {
232        self.inner.is_enabled(level)
233    }
234
235    /// for macros logger_XXX
236    #[doc(hidden)]
237    #[inline(always)]
238    fn _private_api_log(
239        &self, args: fmt::Arguments, level: Level,
240        &(target, module_path, file, line): &(&str, &str, &str, u32),
241    ) {
242        let record = RecordBuilder::new()
243            .level(level)
244            .target(target)
245            .module_path(Some(module_path))
246            .file(Some(file))
247            .line(Some(line))
248            .key_values(&self)
249            .args(args)
250            .build();
251        logger().log(&record);
252    }
253}