captains_log/
lib.rs

1//! # captains-log
2//!
3//! A light-weight logger for rust, implementation base on the crate `log`.
4//!
5//! ## Features
6//!
7//! * Allow customize log format and time format. Refer to [LogFormat].
8//!
9//! * Supports multiple types of sink stacking, each with its own log level.
10//!
11//!     + [Builder::console()] :   Console output to stdout/stderr.
12//!
13//!     + [Builder::raw_file()] :  Support atomic appending from multi-process on linux
14//!
15//! * Log panic message by default.
16//!
17//! * Provide additional [macros](#macros)
18//!
19//! * Supports signal listening for log-rotate. Refer to [Builder::signal()]
20//!
21//! * Provides many preset recipes in [recipe] module for convenience.
22//!
23//! * [Supports configure by environment](#configure-by-environment)
24//!
25//! * [Fine-grain module-level control](#fine-grain-module-level-control)
26//!
27//! * [API-level log handling](#api-level-log-handling)
28//!
29//! * For test suits usage:
30//!
31//!     + Allow dynamic reconfigure logger setting in different test function.
32//!
33//!       Refer to [Unit test example](#unit-test-example).
34//!
35//!     + Provides an attribute macro #\[logfn\] to wrap test function.
36//!
37//!       Refer to [Best practice][#best-practice-with-tests].
38//!
39//! * Provides a [LogParser](crate::parser::LogParser) to work on your log files.
40//!
41//! ## Usage
42//!
43//! Cargo.toml
44//!
45//! ``` toml
46//! [dependencies]
47//! log = { version = "0.4", features = ["std", "kv_unstable"] }
48//! captains_log = "0.5"
49//! ```
50//!
51//! lib.rs or main.rs:
52//!
53//! ``` rust
54//!
55//! // By default, reexport the macros from log crate
56//! #[macro_use]
57//! extern crate captains_log;
58//!
59//! ```
60//!
61//! ## Fast setup examples
62//!
63//! You can refer to various preset recipe in [recipe] module.
64//!
65//! The following is setup two log files for different log-level:
66//!
67//! ``` rust
68//! #[macro_use]
69//! extern crate captains_log;
70//! use captains_log::recipe;
71//!
72//! // You'll get /tmp/test.log with all logs, and /tmp/test.log.wf only with error logs.
73//! let mut log_builder = recipe::split_error_file_logger("/tmp", "test", log::Level::Debug);
74//! // Builder::build() is equivalent of setup_log().
75//! log_builder.build();
76//!
77//! // non-error msg will only appear in /tmp/test.log
78//! debug!("Set a course to Sol system");
79//! info!("Engage");
80//!
81//! // will appear in both /tmp/test.log and /tmp/test.log.wf
82//! error!("Engine over heat!");
83//! ```
84//!
85//! ## Configure by environment
86//!
87//! There is a recipe [env_logger()](crate::recipe::env_logger()) to configure a file logger or
88//! console logger from env. As simple as:
89//!
90//! ``` rust
91//! use captains_log::recipe;
92//! let _ = recipe::env_logger("LOG_FILE", "LOG_LEVEL").build();
93//! ```
94//!
95//! ## Customize format example
96//!
97//! ``` rust
98//! use captains_log::*;
99//!
100//! fn format_f(r: FormatRecord) -> String {
101//!     let time = r.time();
102//!     let level = r.level();
103//!     let file = r.file();
104//!     let line = r.line();
105//!     let msg = r.msg();
106//!     format!("{time}|{level}|{file}:{line}|{msg}\n").to_string()
107//! }
108//! let debug_format = LogFormat::new(
109//!     "%Y%m%d %H:%M:%S%.6f",
110//!     format_f,
111//! );
112//! let debug_file = LogRawFile::new(
113//!     "/tmp", "test.log", log::Level::Trace, debug_format);
114//! let config = Builder::default()
115//!     .signal(signal_hook::consts::SIGINT)
116//!     .raw_file(debug_file);
117//! config.build();
118//! ```
119//!
120//!
121//! If you want to custom more, setup your config with [env_or] helper.
122//!
123//! ## Fine-grain module-level control
124//!
125//! Place [LogFilter] in Arc and share among coroutines.
126//! Log level can be changed on-the-fly.
127//!
128//! There're a set of macro "logger_XXX" to work with `LogFilter`.
129//!
130//! ``` rust
131//! use std::sync::Arc;
132//! use captains_log::*;
133//! log::set_max_level(log::LevelFilter::Debug);
134//! let logger_io = Arc::new(LogFilter::new());
135//! let logger_req = Arc::new(LogFilter::new());
136//! logger_io.set_level(log::Level::Error);
137//! logger_req.set_level(log::Level::Debug);
138//! logger_debug!(logger_req, "Begin handle req ...");
139//! logger_debug!(logger_io, "Issue io to disk ...");
140//! logger_error!(logger_req, "Req invalid ...");
141//! ```
142//!
143//!
144//! ## API-level log handling
145//!
146//! Request log can be track by customizable key (for example, "req_id"), which kept in [LogFilterKV],
147//! and `LogFilterKV` is inherit from `LogFilter`.
148//! You need macro "logger_XXX" to work with it.
149//!
150//! ``` rust
151//! use captains_log::*;
152//! fn debug_format_req_id_f(r: FormatRecord) -> String {
153//!     let time = r.time();
154//!     let level = r.level();
155//!     let file = r.file();
156//!     let line = r.line();
157//!     let msg = r.msg();
158//!     let req_id = r.key("req_id");
159//!     format!("[{time}][{level}][{file}:{line}] {msg}{req_id}\n").to_string()
160//! }
161//! let builder = recipe::raw_file_logger_custom("/tmp/log_filter.log", log::Level::Debug,
162//!     recipe::DEFAULT_TIME, debug_format_req_id_f);
163//! builder.build().expect("setup_log");
164//! let logger = LogFilterKV::new("req_id", format!("{:016x}", 123).to_string());
165//! info!("API service started");
166//! logger_debug!(logger, "Req / received");
167//! logger_debug!(logger, "header xxx");
168//! logger_info!(logger, "Req / 200 complete");
169//! ```
170//!
171//! The log will be:
172//!
173//! ``` text
174//! [2025-06-11 14:33:08.089090][DEBUG][request.rs:67] API service started
175//! [2025-06-11 14:33:10.099092][DEBUG][request.rs:67] Req / received (000000000000007b)
176//! [2025-06-11 14:33:10.099232][WARN][request.rs:68] header xxx (000000000000007b)
177//! [2025-06-11 14:33:11.009092][DEBUG][request.rs:67] Req / 200 complete (000000000000007b)
178//! ```
179//!
180//!
181//! ## Unit test example
182//!
183//! To setup different log config on different tests.
184//!
185//! **Make sure that you call [Builder::test()]** in test cases.
186//! which enable dynamic log config and disable signal_hook.
187//!
188//! ```rust
189//!
190//! use captains_log::*;
191//!
192//! #[test]
193//! fn test1() {
194//!     recipe::raw_file_logger(
195//!         "/tmp/test1.log", Level::Debug).test().build();
196//!     info!("doing test1");
197//! }
198//!
199//! #[test]
200//! fn test2() {
201//!     recipe::raw_file_logger(
202//!         "/tmp/test2.log", Level::Debug).test().build();
203//!     info!("doing test2");
204//! }
205//! ```
206//!
207//! ## Best practice with tests
208//!
209//! We provides proc macro [logfn], the following example shows how to combine with rstest.
210//!
211//! * When you have large test suit, you want to know which logs belong to which test case.
212//!
213//! * Sometimes your test crashes, you want to find the responsible test case.
214//!
215//! * The time spend in each test.
216//!
217//! ``` rust
218//!
219//! use rstest::*;
220//! use captains_log::*;
221//!
222//! // A show case that setup() fixture will be called twice, before each test.
223//! // In order make logs available.
224//! #[fixture]
225//! fn setup() {
226//!     let builder = recipe::raw_file_logger("/tmp/log_rstest.log", log::Level::Debug).test();
227//!     builder.build().expect("setup_log");
228//! }
229//!
230//! #[logfn]
231//! #[rstest(file_size, case(0), case(1))]
232//! fn test_rstest_foo(setup: (), file_size: usize) {
233//!     info!("do something111");
234//! }
235//!
236//! #[logfn]
237//! #[rstest]
238//! fn test_rstest_bar(setup: ()) {
239//!     info!("do something222");
240//! }
241//!
242//! // NOTE rstest must be at the bottom to make fixture effective
243//! #[tokio::test]
244//! #[logfn]
245//! #[rstest]
246//! async fn test_rstest_async(setup: ()) {
247//!     info!("something333")
248//! }
249//! ```
250//!
251//! **Notice:** the order when combine tokio::test with rstest,
252//! `#[rstest]` attribute must be at the bottom to make setup fixture effective.
253//!
254//! After running the test with:
255//!
256//! cargo test -- --test-threads=1
257//!
258//! /tmp/log_rstest.log will have this content:
259//!
260//! ``` text
261//! [2025-07-13 18:22:39.159642][INFO][test_rstest.rs:33] <<< test_rstest_async (setup = ()) enter <<<
262//! [2025-07-13 18:22:39.160255][INFO][test_rstest.rs:37] something333
263//! [2025-07-13 18:22:39.160567][INFO][test_rstest.rs:33] >>> test_rstest_async return () in 564.047µs >>>
264//! [2025-07-13 18:22:39.161299][INFO][test_rstest.rs:26] <<< test_rstest_bar (setup = ()) enter <<<
265//! [2025-07-13 18:22:39.161643][INFO][test_rstest.rs:29] do something222
266//! [2025-07-13 18:22:39.161703][INFO][test_rstest.rs:26] >>> test_rstest_bar return () in 62.681µs >>>
267//! [2025-07-13 18:22:39.162169][INFO][test_rstest.rs:20] <<< test_rstest_foo (setup = (), file_size = 0) enter <<<
268//! [2025-07-13 18:22:39.162525][INFO][test_rstest.rs:23] do something111
269//! [2025-07-13 18:22:39.162600][INFO][test_rstest.rs:20] >>> test_rstest_foo return () in 78.457µs >>>
270//! [2025-07-13 18:22:39.163050][INFO][test_rstest.rs:20] <<< test_rstest_foo (setup = (), file_size = 1) enter <<<
271//! [2025-07-13 18:22:39.163320][INFO][test_rstest.rs:23] do something111
272//! [2025-07-13 18:22:39.163377][INFO][test_rstest.rs:20] >>> test_rstest_foo return () in 58.747µs >>>
273//! ```
274
275extern crate captains_log_helper;
276extern crate log;
277extern crate signal_hook;
278
279#[macro_use]
280extern crate enum_dispatch;
281
282mod config;
283mod console_impl;
284mod file_impl;
285mod formatter;
286mod log_impl;
287mod time;
288
289pub mod macros;
290pub mod parser;
291pub mod recipe;
292
293mod log_filter;
294
295pub use self::{config::*, formatter::FormatRecord, log_filter::*, log_impl::setup_log};
296pub use captains_log_helper::logfn;
297
298pub use log::{Level, LevelFilter};
299pub use log::{debug, error, info, trace, warn};
300
301#[cfg(test)]
302mod tests;