captains_log/lib.rs
1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![cfg_attr(docsrs, allow(unused_attributes))]
3
4//! # captains-log
5//!
6//! A minimalist, customizable, easy to use logger for rust, based on the `log` crate, also adapted to `tracing`,
7//! for production and testing scenario.
8//!
9//! ## Features
10//!
11//! * Allow customize log format and time format. Refer to [LogFormat].
12//!
13//! * Support [subscribing log from tracing](#tracing-support) (**feature** `tracing`)
14//!
15//! * Supports multiple types of sink stacking, each with its own log level.
16//!
17//! + [LogConsole]: Console output to stdout/stderr.
18//!
19//! + [LogRawFile]: Support atomic appending from multi-process on linux (with ext4, xfs)
20//!
21//! + [LogBufFile]: Write to log file with merged I/O and delay flush, and optional self-rotation.
22//!
23//! + [Syslog]: (**feature** `syslog`)
24//!
25//! Write to local or remote syslog server, with timeout and auto reconnect.
26//!
27//! + [LogRingFile]: (**feature** `ringfile`)
28//!
29//! For deadlock / race condition debugging,
30//! collect log to ring buffer in memory. See the doc of [LogRingFile] for how to use.
31//!
32//! * Log panic message by default.
33//!
34//! * Provide additional [macros](#macros), for example: log_assert!(), logger_assert!() ..
35//!
36//! * Supports signal listening for log-rotate. Refer to [Builder::signal()]
37//!
38//! * Provides many preset recipes in [recipe] module for convenience.
39//!
40//! * [Supports configure by environment](#configure-by-environment)
41//!
42//! * [Fine-grain module-level control](#fine-grain-module-level-control)
43//!
44//! * [API-level log handling](#api-level-log-handling)
45//!
46//! * For test suits usage:
47//!
48//! + Allow dynamic reconfigure logger setting in different test function.
49//!
50//! Refer to [Unit test example](#unit-test-example).
51//!
52//! + Provides an attribute macro #\[logfn\] to wrap test function.
53//!
54//! Refer to [Best practice with rstest](#best-practice-with-rstest).
55//!
56//! * Provides a [LogParser](crate::parser::LogParser) to work on your log files.
57//!
58//! ## Usage and futures
59//!
60//! Cargo.toml
61//!
62//! ``` toml
63//! [dependencies]
64//! log = { version = "0.4", features = ["std", "kv_unstable"] }
65//! captains_log = "0.11"
66//! ```
67//!
68//! lib.rs or main.rs:
69//!
70//! ``` rust
71//! // By default, reexport the macros from log crate
72//! #[macro_use]
73//! extern crate captains_log;
74//! ```
75//!
76//! **Optional feature flags**:
77//!
78//!- `syslog`: Enable [Syslog] sink
79//!
80//!- `ringfile`: Enable [LogRingFile] sink
81//!
82//!- `tracing`: Receive log from tracing
83//!
84//! ## Recipes
85//!
86//! You can refer to various preset recipe in [recipe] module.
87//!
88//! Buffered sink with log rotation (See the definition of [Rotation]):
89//!
90//! ``` rust
91//! #[macro_use]
92//! extern crate captains_log;
93//! use captains_log::*;
94//! // rotate when log file reaches 512M. Keep max 10 archiveed files, with recent 2 not compressed.
95//! // All archived log is moved to "/tmp/rotation/old"
96//! let rotation = Rotation::by_size(
97//! 512 * 1024 * 1024, Some(10))
98//! .compress_exclude(2).archive_dir("/tmp/rotation/old");
99//! let _ = recipe::buffered_rotated_file_logger(
100//! "/tmp/rotation.log", Level::Debug, rotation
101//! ).build();
102//! ```
103//!
104//! The following is setup two log files for different log-level:
105//!
106//! ``` rust
107//! #[macro_use]
108//! extern crate captains_log;
109//! use captains_log::recipe;
110//!
111//! // You'll get /tmp/test.log with all logs, and /tmp/test.log.wf only with error logs.
112//! let log_builder = recipe::split_error_file_logger("/tmp", "test", log::Level::Debug);
113//! // Builder::build() is equivalent of setup_log().
114//! log_builder.build();
115//! // non-error msg will only appear in /tmp/test.log
116//! debug!("Set a course to Sol system");
117//! info!("Engage");
118//! // will appear in both /tmp/test.log and /tmp/test.log.wf
119//! error!("Engine over heat!");
120//! ```
121//!
122//! ## Tracing support
123//!
124//! If you want to log tracing events (either in your code or 3rd-party crate), just enable the **`tracing` feature**.
125//!
126//! The message from tracing will use the same log format as defined in [LogFormat].
127//!
128//! We suggest you should **opt out `tracing-log` from default feature-flag of `tracing_subscriber`**,
129//! as it will conflict with captains-log. (It's not allowed to call `log::set_logger()` twice)
130//!
131//! 1) Set global dispatcher (recommended)
132//!
133//! Just turn of the flag `tracing_global` in [Builder], then it will setup [GlobalLogger] as the
134//! default Subscriber.
135//!
136//! Error will be thrown by build() if other default subscribe has been set in tracing.
137//!
138//! ``` rust
139//! use captains_log::*;
140//! recipe::raw_file_logger("/tmp/mylog.log", Level::Debug)
141//! .tracing_global()
142//! .build().expect("setup log");
143//! ```
144//!
145//! 2) Stacking multiple layers (alternative)
146//!
147//! you can choose this method when you need 3rd-party layer
148//! implementation. See the doc of [GlobalLogger::tracing_layer()]
149//!
150//! 3) Subscribe to tracing in the scope (rarely used).
151//!
152//! Assume you have a different tracing global dispatcher, but want to output to captains_log in
153//! the scope. See the doc of [GlobalLogger::tracing_dispatch()]
154//!
155//! ## Configure by environment
156//!
157//! There is a recipe [env_logger()](crate::recipe::env_logger()) to configure a file logger or
158//! console logger from env. As simple as:
159//!
160//! ``` rust
161//! use captains_log::recipe;
162//! recipe::env_logger("LOG_FILE", "LOG_LEVEL").build().expect("setup log");
163//! ```
164//!
165//! If you want to custom more, setup your config with [env_or] helper.
166//!
167//! ## Customize format example
168//!
169//! ``` rust
170//! use captains_log::*;
171//!
172//! fn format_f(r: FormatRecord) -> String {
173//! let time = r.time();
174//! let level = r.level();
175//! let file = r.file();
176//! let line = r.line();
177//! let msg = r.msg();
178//! format!("{time}|{level}|{file}:{line}|{msg}\n").to_string()
179//! }
180//! let debug_format = LogFormat::new(
181//! "%Y%m%d %H:%M:%S%.6f",
182//! format_f,
183//! );
184//! let debug_file = LogRawFile::new(
185//! "/tmp", "test.log", log::Level::Trace, debug_format,
186//! );
187//! let config = Builder::default()
188//! .signal(signal_hook::consts::SIGINT)
189//! .add_sink(debug_file);
190//! config.build();
191//! ```
192//!
193//! ## Fine-grain module-level control
194//!
195//! Place [LogFilter] in Arc and share among coroutines.
196//! Log level can be changed on-the-fly.
197//!
198//! There're a set of macro "logger_XXX" to work with `LogFilter`.
199//!
200//! ``` rust
201//! use std::sync::Arc;
202//! use captains_log::*;
203//! log::set_max_level(log::LevelFilter::Debug);
204//! let logger_io = Arc::new(LogFilter::new());
205//! let logger_req = Arc::new(LogFilter::new());
206//! logger_io.set_level(log::Level::Error);
207//! logger_req.set_level(log::Level::Debug);
208//! logger_debug!(logger_req, "Begin handle req ...");
209//! logger_debug!(logger_io, "Issue io to disk ...");
210//! logger_error!(logger_req, "Req invalid ...");
211//! ```
212//!
213//!
214//! ## API-level log handling
215//!
216//! Request log can be track by customizable key (for example, "req_id"), which kept in [LogFilterKV],
217//! and `LogFilterKV` is inherit from `LogFilter`.
218//! You need macro "logger_XXX" to work with it.
219//!
220//! ``` rust
221//! use captains_log::*;
222//! fn debug_format_req_id_f(r: FormatRecord) -> String {
223//! let time = r.time();
224//! let level = r.level();
225//! let file = r.file();
226//! let line = r.line();
227//! let msg = r.msg();
228//! let req_id = r.key("req_id");
229//! format!("[{time}][{level}][{file}:{line}] {msg}{req_id}\n").to_string()
230//! }
231//! let builder = recipe::raw_file_logger_custom(
232//! "/tmp/log_filter.log", log::Level::Debug,
233//! recipe::DEFAULT_TIME, debug_format_req_id_f)
234//! .build().expect("setup log");
235//!
236//! let logger = LogFilterKV::new("req_id", format!("{:016x}", 123).to_string());
237//! info!("API service started");
238//! logger_debug!(logger, "Req / received");
239//! logger_debug!(logger, "header xxx");
240//! logger_info!(logger, "Req / 200 complete");
241//! ```
242//!
243//! The log will be:
244//!
245//! ``` text
246//! [2025-06-11 14:33:08.089090][DEBUG][request.rs:67] API service started
247//! [2025-06-11 14:33:10.099092][DEBUG][request.rs:67] Req / received (000000000000007b)
248//! [2025-06-11 14:33:10.099232][WARN][request.rs:68] header xxx (000000000000007b)
249//! [2025-06-11 14:33:11.009092][DEBUG][request.rs:67] Req / 200 complete (000000000000007b)
250//! ```
251//!
252//!
253//! ## Unit test example
254//!
255//! To setup different log config on different tests.
256//!
257//! **Make sure that you call [Builder::test()]** in test cases.
258//! which enable dynamic log config and disable signal_hook.
259//!
260//! ```rust
261//! use captains_log::*;
262//!
263//! #[test]
264//! fn test1() {
265//! recipe::raw_file_logger(
266//! "/tmp/test1.log", Level::Debug).test().build().expect("setup log");
267//! info!("doing test1");
268//! }
269//!
270//! #[test]
271//! fn test2() {
272//! recipe::raw_file_logger(
273//! "/tmp/test2.log", Level::Debug).test().build().expect("setup log");
274//! info!("doing test2");
275//! }
276//! ```
277//!
278//! ## Best practice with rstest
279//!
280//! We provides proc macro [logfn], the following example shows how to combine with rstest.
281//!
282//! * When you have large test suit, you want to know which logs belong to which test case.
283//!
284//! * Sometimes your test crashes, you want to find the responsible test case.
285//!
286//! * The time spend in each test.
287//!
288//! ``` rust
289//!
290//! use rstest::*;
291//! use captains_log::*;
292//!
293//! // A show case that setup() fixture will be called twice, before each test.
294//! // In order make logs available.
295//! #[fixture]
296//! fn setup() {
297//! let _logger = recipe::raw_file_logger(
298//! "/tmp/log_rstest.log", log::Level::Debug)
299//! .test().build().expect("setup_log");
300//! }
301//!
302//! #[logfn]
303//! #[rstest(file_size, case(0), case(1))]
304//! fn test_rstest_foo(setup: (), file_size: usize) {
305//! info!("do something111");
306//! }
307//!
308//! #[logfn]
309//! #[rstest]
310//! fn test_rstest_bar(setup: ()) {
311//! info!("do something222");
312//! }
313//!
314//! // NOTE rstest must be at the bottom to make fixture effective
315//! #[tokio::test]
316//! #[logfn]
317//! #[rstest]
318//! async fn test_rstest_async(setup: ()) {
319//! info!("something333")
320//! }
321//! ```
322//!
323//! **Notice:** the order when combine tokio::test with rstest,
324//! `#[rstest]` attribute must be at the bottom to make setup fixture effective.
325//!
326//! After running the test with:
327//!
328//! cargo test -- --test-threads=1
329//!
330//! /tmp/log_rstest.log will have this content:
331//!
332//! ``` text
333//! [2025-07-13 18:22:39.159642][INFO][test_rstest.rs:33] <<< test_rstest_async (setup = ()) enter <<<
334//! [2025-07-13 18:22:39.160255][INFO][test_rstest.rs:37] something333
335//! [2025-07-13 18:22:39.160567][INFO][test_rstest.rs:33] >>> test_rstest_async return () in 564.047µs >>>
336//! [2025-07-13 18:22:39.161299][INFO][test_rstest.rs:26] <<< test_rstest_bar (setup = ()) enter <<<
337//! [2025-07-13 18:22:39.161643][INFO][test_rstest.rs:29] do something222
338//! [2025-07-13 18:22:39.161703][INFO][test_rstest.rs:26] >>> test_rstest_bar return () in 62.681µs >>>
339//! [2025-07-13 18:22:39.162169][INFO][test_rstest.rs:20] <<< test_rstest_foo (setup = (), file_size = 0) enter <<<
340//! [2025-07-13 18:22:39.162525][INFO][test_rstest.rs:23] do something111
341//! [2025-07-13 18:22:39.162600][INFO][test_rstest.rs:20] >>> test_rstest_foo return () in 78.457µs >>>
342//! [2025-07-13 18:22:39.163050][INFO][test_rstest.rs:20] <<< test_rstest_foo (setup = (), file_size = 1) enter <<<
343//! [2025-07-13 18:22:39.163320][INFO][test_rstest.rs:23] do something111
344//! [2025-07-13 18:22:39.163377][INFO][test_rstest.rs:20] >>> test_rstest_foo return () in 58.747µs >>>
345//! ```
346
347extern crate captains_log_helper;
348extern crate log;
349extern crate signal_hook;
350
351#[macro_use]
352extern crate enum_dispatch;
353
354mod buf_file_impl;
355mod config;
356mod console_impl;
357mod env;
358mod file_impl;
359mod formatter;
360mod log_impl;
361mod rotation;
362mod time;
363
364#[cfg(feature = "syslog")]
365#[cfg_attr(docsrs, doc(cfg(feature = "syslog")))]
366mod syslog;
367#[cfg(feature = "syslog")]
368#[cfg_attr(docsrs, doc(cfg(feature = "syslog")))]
369pub use self::syslog::*;
370
371#[cfg(feature = "ringfile")]
372#[cfg_attr(docsrs, doc(cfg(feature = "ringfile")))]
373mod ring;
374#[cfg(feature = "ringfile")]
375#[cfg_attr(docsrs, doc(cfg(feature = "ringfile")))]
376pub use self::ring::*;
377
378pub mod macros;
379pub mod parser;
380pub mod recipe;
381
382mod log_filter;
383
384pub use self::buf_file_impl::*;
385pub use self::console_impl::*;
386pub use self::env::*;
387pub use self::file_impl::*;
388pub use self::rotation::*;
389pub use self::{
390 config::*,
391 formatter::FormatRecord,
392 log_filter::*,
393 log_impl::{get_global_logger, setup_log, GlobalLogger},
394};
395pub use captains_log_helper::logfn;
396
397#[cfg(feature = "tracing")]
398mod tracing_bridge;
399#[cfg(feature = "tracing")]
400pub use tracing_bridge::CaptainsLogLayer;
401
402/// Re-export log::Level:
403pub use log::Level;
404/// Re-export log::LevelFilter:
405pub use log::LevelFilter;
406pub use log::{debug, error, info, trace, warn};
407
408/// Re-export from signal_hook::consts:
409pub use signal_hook::consts::signal as signal_consts;