Skip to main content

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::{
26        atomic::{AtomicU8, Ordering},
27        Arc,
28    },
29};
30
31use log::{
32    kv::{Error, Key, ToKey, Value, VisitSource},
33    *,
34};
35
36pub trait Filter: log::kv::Source {
37    /// whether a log level is enable
38    fn is_enabled(&self, _level: Level) -> bool;
39
40    /// for macros logger_XXX
41    #[doc(hidden)]
42    #[inline(always)]
43    fn _private_api_log(
44        &self, args: fmt::Arguments, level: Level,
45        &(target, module_path, file, line): &(&str, &str, &str, u32),
46    ) {
47        let record = RecordBuilder::new()
48            .level(level)
49            .target(target)
50            .module_path(Some(module_path))
51            .file(Some(file))
52            .line(Some(line))
53            .args(args)
54            .build();
55        logger().log(&record);
56    }
57}
58
59impl<T: Filter> Filter for Arc<T> {
60    #[inline(always)]
61    fn is_enabled(&self, _level: Level) -> bool {
62        Filter::is_enabled(self.as_ref(), _level)
63    }
64}
65
66impl<T: Filter> Filter for &T {
67    #[inline(always)]
68    fn is_enabled(&self, _level: Level) -> bool {
69        Filter::is_enabled(*self, _level)
70    }
71}
72
73/// `LogFilter` supports concurrent control the log level filter with atomic.
74///
75/// Usually wrapped with Arc, used in combine with macros logger_XXX. the log level filter can be dynamic changed.
76///
77/// # Example
78///
79/// ``` rust
80/// use std::sync::Arc;
81/// use captains_log::{*, filter::LogFilter};
82/// log::set_max_level(log::LevelFilter::Debug);
83/// let logger_io = Arc::new(LogFilter::new());
84/// let logger_req = Arc::new(LogFilter::new());
85/// logger_io.set_level(log::Level::Error);
86/// logger_req.set_level(log::Level::Debug);
87/// logger_debug!(logger_req, "Begin handle req ...");
88/// logger_debug!(logger_io, "Issue io to disk ...");
89/// logger_error!(logger_req, "Req invalid ...");
90/// ```
91pub struct LogFilter {
92    max_level: AtomicU8,
93}
94
95impl LogFilter {
96    pub fn new() -> Self {
97        Self { max_level: AtomicU8::new(Level::Trace as u8) }
98    }
99
100    /// When LogFilter is shared in Arc, allows concurrently changing log level filter
101    #[inline]
102    pub fn set_level(&self, level: Level) {
103        self.max_level.store(level as u8, Ordering::Relaxed);
104    }
105
106    #[inline]
107    pub fn get_level(&self) -> u8 {
108        self.max_level.load(Ordering::Relaxed)
109    }
110}
111
112impl Filter for LogFilter {
113    #[inline(always)]
114    fn is_enabled(&self, level: Level) -> bool {
115        level as u8 <= self.max_level.load(Ordering::Relaxed)
116    }
117}
118
119impl log::kv::Source for LogFilter {
120    #[inline(always)]
121    fn visit<'kvs>(&'kvs self, _visitor: &mut dyn VisitSource<'kvs>) -> Result<(), Error> {
122        Ok(())
123    }
124
125    #[inline(always)]
126    fn get<'a>(&'a self, _key: Key) -> Option<Value<'a>> {
127        return None;
128    }
129
130    #[inline(always)]
131    fn count(&self) -> usize {
132        0
133    }
134}
135
136/// GlobalFilter use static reference to AtomicU8 to avoid cloning cost of `Arc<LogFilter>`
137#[derive(Clone)]
138pub struct GlobalFilter {
139    max_level: &'static AtomicU8,
140}
141
142impl GlobalFilter {
143    pub fn new(max_level: &'static AtomicU8) -> Self {
144        Self { max_level }
145    }
146
147    /// When LogFilter is shared in Arc, allows concurrently changing log level filter
148    #[inline]
149    pub fn set_level(&self, level: Level) {
150        self.max_level.store(level as u8, Ordering::Relaxed);
151    }
152
153    #[inline]
154    pub fn get_level(&self) -> u8 {
155        self.max_level.load(Ordering::Relaxed)
156    }
157}
158
159impl Filter for GlobalFilter {
160    #[inline(always)]
161    fn is_enabled(&self, level: Level) -> bool {
162        level as u8 <= self.max_level.load(Ordering::Relaxed)
163    }
164}
165
166impl log::kv::Source for GlobalFilter {
167    #[inline(always)]
168    fn visit<'kvs>(&'kvs self, _visitor: &mut dyn VisitSource<'kvs>) -> Result<(), Error> {
169        Ok(())
170    }
171
172    #[inline(always)]
173    fn get<'a>(&'a self, _key: Key) -> Option<Value<'a>> {
174        return None;
175    }
176
177    #[inline(always)]
178    fn count(&self) -> usize {
179        0
180    }
181}
182
183/// A Filter that enables all log levels
184#[derive(Default)]
185pub struct DummyFilter();
186
187impl DummyFilter {
188    #[inline(always)]
189    pub fn new() -> Self {
190        DummyFilter()
191    }
192}
193
194impl Filter for DummyFilter {
195    #[inline(always)]
196    fn is_enabled(&self, _level: Level) -> bool {
197        true
198    }
199}
200
201impl log::kv::Source for DummyFilter {
202    #[inline(always)]
203    fn visit<'kvs>(&'kvs self, _visitor: &mut dyn VisitSource<'kvs>) -> Result<(), Error> {
204        Ok(())
205    }
206
207    #[inline(always)]
208    fn get<'a>(&'a self, _key: Key) -> Option<Value<'a>> {
209        return None;
210    }
211
212    #[inline(always)]
213    fn count(&self) -> usize {
214        0
215    }
216}
217
218/// `KeyFilter` is wrapper from [Filter], with one additional key into log format.
219///
220/// The name of the key can be customized.
221///
222/// Example for an http service, api handling log will have a field `req_id`.
223/// When you received error from one of the request,
224/// you can grep all the relevant log with that `req_id`.
225///
226/// ``` rust
227/// use captains_log::{*, filter::{LogFilter, KeyFilter}};
228/// use std::sync::Arc;
229/// fn debug_format_req_id_f(r: FormatRecord) -> String {
230///     let time = r.time();
231///     let level = r.level();
232///     let file = r.file();
233///     let line = r.line();
234///     let msg = r.msg();
235///     let req_id = r.key("req_id");
236///     format!("[{time}][{level}][{file}:{line}] {msg}{req_id}\n").to_string()
237/// }
238/// let builder = recipe::raw_file_logger_custom(
239///                 "/tmp/log_filter.log", log::Level::Debug,
240///                 recipe::DEFAULT_TIME, debug_format_req_id_f)
241///     .build().expect("setup log");
242///
243/// // Wrapping and Arc
244/// let filter = Arc::new(LogFilter::new());
245/// let logger = KeyFilter::with(filter.clone(), "req_id", format!("{:016x}", 123).to_string());
246/// info!("API service started");
247/// logger_debug!(logger, "Req / received");
248/// logger_debug!(logger, "header xxx");
249/// logger_info!(logger, "Req / 200 complete");
250///
251/// ```
252///
253/// The log will be:
254///
255/// ``` text
256/// [2025-06-11 14:33:08.089090][DEBUG][request.rs:67] API service started
257/// [2025-06-11 14:33:10.099092][DEBUG][request.rs:67] Req / received (000000000000007b)
258/// [2025-06-11 14:33:10.099232][WARN][request.rs:68] header xxx (000000000000007b)
259/// [2025-06-11 14:33:11.009092][DEBUG][request.rs:67] Req / 200 complete (000000000000007b)
260/// ```
261///
262/// Using reference:
263///
264/// ```rust
265/// use captains_log::{*, filter::{LogFilter, KeyFilter}};
266/// let filter = LogFilter::new();
267/// let logger = KeyFilter::with(&filter, "req_id", format!("{:016x}", 123).to_string());
268/// logger_debug!(logger, "Req / received");
269/// ```
270pub struct KeyFilter<T, V>
271where
272    T: Filter,
273    V: log::kv::ToValue,
274{
275    inner: T,
276    key: &'static str,
277    value: V,
278}
279
280impl<T, V> Clone for KeyFilter<T, V>
281where
282    T: Filter + Clone,
283    V: log::kv::ToValue + Clone,
284{
285    #[inline]
286    fn clone(&self) -> Self {
287        Self { inner: self.inner.clone(), key: self.key, value: self.value.clone() }
288    }
289}
290
291impl<T, V> Deref for KeyFilter<T, V>
292where
293    T: Filter,
294    V: log::kv::ToValue,
295{
296    type Target = T;
297    #[inline]
298    fn deref(&self) -> &T {
299        &self.inner
300    }
301}
302
303impl<T, V> KeyFilter<T, V>
304where
305    T: Filter,
306    V: log::kv::ToValue,
307{
308    #[inline]
309    pub fn with(inner: T, key: &'static str, value: V) -> Self {
310        Self { inner, key, value }
311    }
312}
313
314impl<T, V> log::kv::Source for KeyFilter<T, V>
315where
316    T: Filter,
317    V: log::kv::ToValue,
318{
319    #[inline(always)]
320    fn visit<'kvs>(&'kvs self, visitor: &mut dyn VisitSource<'kvs>) -> Result<(), Error> {
321        visitor.visit_pair(self.key.to_key(), self.value.to_value())
322    }
323
324    #[inline(always)]
325    fn get<'a>(&'a self, key: Key) -> Option<Value<'a>> {
326        if key.as_ref() == self.key {
327            return Some(self.value.to_value());
328        }
329        return None;
330    }
331
332    #[inline(always)]
333    fn count(&self) -> usize {
334        1
335    }
336}
337
338impl<T, V> Filter for KeyFilter<T, V>
339where
340    T: Filter,
341    V: log::kv::ToValue,
342{
343    #[inline(always)]
344    fn is_enabled(&self, level: Level) -> bool {
345        self.inner.is_enabled(level)
346    }
347
348    /// for macros logger_XXX
349    #[doc(hidden)]
350    #[inline(always)]
351    fn _private_api_log(
352        &self, args: fmt::Arguments, level: Level,
353        &(target, module_path, file, line): &(&str, &str, &str, u32),
354    ) {
355        // Add key_values, which LogFilter does not.
356        let record = RecordBuilder::new()
357            .level(level)
358            .target(target)
359            .module_path(Some(module_path))
360            .file(Some(file))
361            .line(Some(line))
362            .key_values(&self)
363            .args(args)
364            .build();
365        logger().log(&record);
366    }
367}
368
369impl<V> KeyFilter<DummyFilter, V>
370where
371    V: log::kv::ToValue,
372{
373    #[inline]
374    pub fn new(key: &'static str, value: V) -> Self {
375        Self { inner: DummyFilter(), key, value }
376    }
377}