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