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 `LogFilterKV`, 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 [LogFilterKV] for details.
20
21use std::{
22    fmt, str,
23    sync::atomic::{AtomicUsize, Ordering},
24};
25
26use log::{kv::*, *};
27
28/// `LogFilter` supports concurrent control the log level filter with atomic.
29///
30/// Used in combine with macros logger_XXX. the log level filter can be dynamic changed.
31///
32/// # Example
33///
34/// ``` rust
35/// use std::sync::Arc;
36/// use captains_log::{*, filter::LogFilter};
37/// log::set_max_level(log::LevelFilter::Debug);
38/// let logger_io = Arc::new(LogFilter::new());
39/// let logger_req = Arc::new(LogFilter::new());
40/// logger_io.set_level(log::Level::Error);
41/// logger_req.set_level(log::Level::Debug);
42/// logger_debug!(logger_req, "Begin handle req ...");
43/// logger_debug!(logger_io, "Issue io to disk ...");
44/// logger_error!(logger_req, "Req invalid ...");
45/// ```
46pub struct LogFilter {
47    max_level: AtomicUsize,
48}
49
50impl Clone for LogFilter {
51    fn clone(&self) -> Self {
52        Self { max_level: AtomicUsize::new(self.get_level()) }
53    }
54}
55
56impl LogFilter {
57    pub fn new() -> Self {
58        Self { max_level: AtomicUsize::new(Level::Trace as usize) }
59    }
60
61    /// When LogFilter is shared in Arc, allows concurrently changing log level filter
62    #[inline]
63    pub fn set_level(&self, level: Level) {
64        self.max_level.store(level as usize, Ordering::Relaxed);
65    }
66
67    #[inline]
68    pub fn get_level(&self) -> usize {
69        self.max_level.load(Ordering::Relaxed)
70    }
71
72    /// for macros logger_XXX
73    #[doc(hidden)]
74    #[inline(always)]
75    pub fn _private_api_log(
76        &self, args: fmt::Arguments, level: Level,
77        &(target, module_path, file, line): &(&str, &str, &str, u32),
78    ) {
79        let record = RecordBuilder::new()
80            .level(level)
81            .target(target)
82            .module_path(Some(module_path))
83            .file(Some(file))
84            .line(Some(line))
85            .args(args)
86            .build();
87        logger().log(&record);
88    }
89}
90
91impl log::kv::Source for LogFilter {
92    #[inline(always)]
93    fn visit<'kvs>(&'kvs self, _visitor: &mut dyn Visitor<'kvs>) -> Result<(), Error> {
94        Ok(())
95    }
96
97    #[inline(always)]
98    fn get<'a>(&'a self, _key: Key) -> Option<Value<'a>> {
99        return None;
100    }
101
102    #[inline(always)]
103    fn count(&self) -> usize {
104        0
105    }
106}
107
108/// `LogFilterKV` is inherited from [LogFilter], with one additional key into log format.
109///
110/// The name of the key can be customized.
111///
112/// Example for an http service, api handling log will have a field `req_id`.
113/// When you received error from one of the request,
114/// you can grep all the relevant log with that `req_id`.
115///
116/// ``` rust
117/// use captains_log::{*, filter::LogFilterKV};
118/// fn debug_format_req_id_f(r: FormatRecord) -> String {
119///     let time = r.time();
120///     let level = r.level();
121///     let file = r.file();
122///     let line = r.line();
123///     let msg = r.msg();
124///     let req_id = r.key("req_id");
125///     format!("[{time}][{level}][{file}:{line}] {msg}{req_id}\n").to_string()
126/// }
127/// let builder = recipe::raw_file_logger_custom(
128///                 "/tmp/log_filter.log", log::Level::Debug,
129///                 recipe::DEFAULT_TIME, debug_format_req_id_f)
130///     .build().expect("setup log");
131///
132/// let logger = LogFilterKV::new("req_id", format!("{:016x}", 123).to_string());
133/// info!("API service started");
134/// logger_debug!(logger, "Req / received");
135/// logger_debug!(logger, "header xxx");
136/// logger_info!(logger, "Req / 200 complete");
137/// ```
138///
139/// The log will be:
140///
141/// ``` text
142/// [2025-06-11 14:33:08.089090][DEBUG][request.rs:67] API service started
143/// [2025-06-11 14:33:10.099092][DEBUG][request.rs:67] Req / received (000000000000007b)
144/// [2025-06-11 14:33:10.099232][WARN][request.rs:68] header xxx (000000000000007b)
145/// [2025-06-11 14:33:11.009092][DEBUG][request.rs:67] Req / 200 complete (000000000000007b)
146/// ```
147#[derive(Clone)]
148pub struct LogFilterKV {
149    inner: LogFilter,
150    key: &'static str,
151    value: String,
152}
153
154impl LogFilterKV {
155    pub fn new(key: &'static str, value: String) -> Self {
156        Self { inner: LogFilter::new(), key, value }
157    }
158
159    /// When LogFilter is shared in Arc, allows concurrently changing log level filter
160    #[inline]
161    pub fn set_level(&self, level: Level) {
162        self.inner.set_level(level)
163    }
164
165    #[inline]
166    pub fn get_level(&self) -> usize {
167        self.inner.get_level()
168    }
169
170    /// for macros logger_XXX
171    #[doc(hidden)]
172    #[inline(always)]
173    pub fn _private_api_log(
174        &self, args: fmt::Arguments, level: Level,
175        &(target, module_path, file, line): &(&str, &str, &str, u32),
176    ) {
177        let record = RecordBuilder::new()
178            .level(level)
179            .target(target)
180            .module_path(Some(module_path))
181            .file(Some(file))
182            .line(Some(line))
183            .key_values(&self)
184            .args(args)
185            .build();
186        logger().log(&record);
187    }
188}
189
190impl log::kv::Source for LogFilterKV {
191    #[inline(always)]
192    fn visit<'kvs>(&'kvs self, visitor: &mut dyn Visitor<'kvs>) -> Result<(), Error> {
193        visitor.visit_pair(self.key.to_key(), self.value.as_str().into())
194    }
195
196    #[inline(always)]
197    fn get<'a>(&'a self, key: Key) -> Option<Value<'a>> {
198        if key.as_ref() == self.key {
199            return Some(self.value.as_str().into());
200        }
201        return None;
202    }
203
204    #[inline(always)]
205    fn count(&self) -> usize {
206        1
207    }
208}