Crate captains_log

Crate captains_log 

Source
Expand description

§captains-log

A minimalist, customizable, easy to use logger for rust, based on the log crate, also adapted to tracing, for production and testing scenario.

§Features

§Usage and futures

Cargo.toml

[dependencies]
log = { version = "0.4", features = ["std", "kv_unstable"] }
captains_log = "0.11"

lib.rs or main.rs:

// By default, reexport the macros from log crate
#[macro_use]
extern crate captains_log;

Optional feature flags:

  • syslog: Enable Syslog sink

  • ringfile: Enable LogRingFile sink

  • tracing: Receive log from tracing

§Recipes

You can refer to various preset recipe in recipe module.

Buffered sink with log rotation (See the definition of Rotation):

#[macro_use]
extern crate captains_log;
use captains_log::*;
// rotate when log file reaches 512M. Keep max 10 archiveed files, with recent 2 not compressed.
// All archived log is moved to "/tmp/rotation/old"
let rotation = Rotation::by_size(
        512 * 1024 * 1024, Some(10))
    .compress_exclude(2).archive_dir("/tmp/rotation/old");
let _ = recipe::buffered_rotated_file_logger(
            "/tmp/rotation.log", Level::Debug, rotation
).build();

The following is setup two log files for different log-level:

#[macro_use]
extern crate captains_log;
use captains_log::recipe;

// You'll get /tmp/test.log with all logs, and /tmp/test.log.wf only with error logs.
let log_builder = recipe::split_error_file_logger("/tmp", "test", log::Level::Debug);
// Builder::build() is equivalent of setup_log().
log_builder.build();
// non-error msg will only appear in /tmp/test.log
debug!("Set a course to Sol system");
info!("Engage");
// will appear in both /tmp/test.log and /tmp/test.log.wf
error!("Engine over heat!");

§Tracing support

If you want to log tracing events (either in your code or 3rd-party crate), just enable the tracing feature.

The message from tracing will use the same log format as defined in LogFormat.

We suggest you should opt out tracing-log from default feature-flag of tracing_subscriber, as it will conflict with captains-log. (It’s not allowed to call log::set_logger() twice)

  1. Set global dispatcher (recommended)

Just turn of the flag tracing_global in Builder, then it will setup GlobalLogger as the default Subscriber.

Error will be thrown by build() if other default subscribe has been set in tracing.

use captains_log::*;
recipe::raw_file_logger("/tmp/mylog.log", Level::Debug)
                    .tracing_global()
                   .build().expect("setup log");
  1. Stacking multiple layers (alternative)

you can choose this method when you need 3rd-party layer implementation. See the doc of GlobalLogger::tracing_layer()

  1. Subscribe to tracing in the scope (rarely used).

Assume you have a different tracing global dispatcher, but want to output to captains_log in the scope. See the doc of GlobalLogger::tracing_dispatch()

§Configure by environment

There is a recipe env_logger() to configure a file logger or console logger from env. As simple as:

use captains_log::recipe;
recipe::env_logger("LOG_FILE", "LOG_LEVEL").build().expect("setup log");

If you want to custom more, setup your config with env_or helper.

§Customize format example

use captains_log::*;

fn format_f(r: FormatRecord) -> String {
    let time = r.time();
    let level = r.level();
    let file = r.file();
    let line = r.line();
    let msg = r.msg();
    format!("{time}|{level}|{file}:{line}|{msg}\n").to_string()
}
let debug_format = LogFormat::new(
    "%Y%m%d %H:%M:%S%.6f",
    format_f,
);
let debug_file = LogRawFile::new(
    "/tmp", "test.log", log::Level::Trace, debug_format,
);
let config = Builder::default()
    .signal(signal_hook::consts::SIGINT)
    .add_sink(debug_file);
config.build();

§Fine-grain module-level control

Place LogFilter in Arc and share among coroutines. Log level can be changed on-the-fly.

There’re a set of macro “logger_XXX” to work with LogFilter.

use std::sync::Arc;
use captains_log::*;
log::set_max_level(log::LevelFilter::Debug);
let logger_io = Arc::new(LogFilter::new());
let logger_req = Arc::new(LogFilter::new());
logger_io.set_level(log::Level::Error);
logger_req.set_level(log::Level::Debug);
logger_debug!(logger_req, "Begin handle req ...");
logger_debug!(logger_io, "Issue io to disk ...");
logger_error!(logger_req, "Req invalid ...");

§API-level log handling

Request log can be track by customizable key (for example, “req_id”), which kept in LogFilterKV, and LogFilterKV is inherit from LogFilter. You need macro “logger_XXX” to work with it.

use captains_log::*;
fn debug_format_req_id_f(r: FormatRecord) -> String {
    let time = r.time();
    let level = r.level();
    let file = r.file();
    let line = r.line();
    let msg = r.msg();
    let req_id = r.key("req_id");
    format!("[{time}][{level}][{file}:{line}] {msg}{req_id}\n").to_string()
}
let builder = recipe::raw_file_logger_custom(
                "/tmp/log_filter.log", log::Level::Debug,
                recipe::DEFAULT_TIME, debug_format_req_id_f)
    .build().expect("setup log");

let logger = LogFilterKV::new("req_id", format!("{:016x}", 123).to_string());
info!("API service started");
logger_debug!(logger, "Req / received");
logger_debug!(logger, "header xxx");
logger_info!(logger, "Req / 200 complete");

The log will be:

[2025-06-11 14:33:08.089090][DEBUG][request.rs:67] API service started
[2025-06-11 14:33:10.099092][DEBUG][request.rs:67] Req / received (000000000000007b)
[2025-06-11 14:33:10.099232][WARN][request.rs:68] header xxx (000000000000007b)
[2025-06-11 14:33:11.009092][DEBUG][request.rs:67] Req / 200 complete (000000000000007b)

§Unit test example

To setup different log config on different tests.

Make sure that you call Builder::test() in test cases. which enable dynamic log config and disable signal_hook.

use captains_log::*;

#[test]
fn test1() {
    recipe::raw_file_logger(
        "/tmp/test1.log", Level::Debug).test().build().expect("setup log");
    info!("doing test1");
}

#[test]
fn test2() {
    recipe::raw_file_logger(
        "/tmp/test2.log", Level::Debug).test().build().expect("setup log");
    info!("doing test2");
}

§Best practice with rstest

We provides proc macro logfn, the following example shows how to combine with rstest.

  • When you have large test suit, you want to know which logs belong to which test case.

  • Sometimes your test crashes, you want to find the responsible test case.

  • The time spend in each test.


use rstest::*;
use captains_log::*;

// A show case that setup() fixture will be called twice, before each test.
// In order make logs available.
#[fixture]
fn setup() {
    let _logger = recipe::raw_file_logger(
        "/tmp/log_rstest.log", log::Level::Debug)
        .test().build().expect("setup_log");
}

#[logfn]
#[rstest(file_size, case(0), case(1))]
fn test_rstest_foo(setup: (), file_size: usize) {
    info!("do something111");
}

#[logfn]
#[rstest]
fn test_rstest_bar(setup: ()) {
    info!("do something222");
}

// NOTE rstest must be at the bottom to make fixture effective
#[tokio::test]
#[logfn]
#[rstest]
async fn test_rstest_async(setup: ()) {
    info!("something333")
}

Notice: the order when combine tokio::test with rstest, #[rstest] attribute must be at the bottom to make setup fixture effective.

After running the test with:

cargo test – –test-threads=1

/tmp/log_rstest.log will have this content:

[2025-07-13 18:22:39.159642][INFO][test_rstest.rs:33] <<< test_rstest_async (setup = ()) enter <<<
[2025-07-13 18:22:39.160255][INFO][test_rstest.rs:37] something333
[2025-07-13 18:22:39.160567][INFO][test_rstest.rs:33] >>> test_rstest_async return () in 564.047µs >>>
[2025-07-13 18:22:39.161299][INFO][test_rstest.rs:26] <<< test_rstest_bar (setup = ()) enter <<<
[2025-07-13 18:22:39.161643][INFO][test_rstest.rs:29] do something222
[2025-07-13 18:22:39.161703][INFO][test_rstest.rs:26] >>> test_rstest_bar return () in 62.681µs >>>
[2025-07-13 18:22:39.162169][INFO][test_rstest.rs:20] <<< test_rstest_foo (setup = (), file_size = 0) enter <<<
[2025-07-13 18:22:39.162525][INFO][test_rstest.rs:23] do something111
[2025-07-13 18:22:39.162600][INFO][test_rstest.rs:20] >>> test_rstest_foo return () in 78.457µs >>>
[2025-07-13 18:22:39.163050][INFO][test_rstest.rs:20] <<< test_rstest_foo (setup = (), file_size = 1) enter <<<
[2025-07-13 18:22:39.163320][INFO][test_rstest.rs:23] do something111
[2025-07-13 18:22:39.163377][INFO][test_rstest.rs:20] >>> test_rstest_foo return () in 58.747µs >>>

Modules§

macros
parser
recipe
The recipe module contains some prelude functions that construct a Builder for convenience use.
signal_consts
Re-export from signal_hook::consts: The signal constants.

Macros§

debug
Logs a message at the debug level.
error
Logs a message at the error level.
info
Logs a message at the info level.
log_assert
Will log and panic when condition not met.
log_assert_eq
Will log and panic when condition not met.
log_debug_assert
On Debug build, will log and panic when condition not met. Skip the check on release build.
log_debug_assert_eq
On Debug build, will log and panic when condition not met. Skip the check on release build.
log_eprintln
log and println to stderr.
log_println
log and println to stdout.
logger_assert
Will log with log_filter and panic when condition not met.
logger_assert_eq
Will log with log_filter and panic when condition not met.
logger_debug
Similar to debug!(), but the first argument is LogFilter or LogFilterKV
logger_debug_assert
On debug build, will log with log_filter and panic when condition not met. Skip the check on release build.
logger_debug_assert_eq
On debug build, will log with log_filter and panic when condition not met. Skip the check on release build.
logger_error
Similar to error!(), but the first argument is LogFilter or LogFilterKV.
logger_info
Similar to info!(), but the first argument is LogFilter or LogFilterKV.
logger_trace
Similar to trace!(), but the first argument is LogFilter or LogFilterKV
logger_warn
Similar to warn!(), but the first argument is LogFilter or LogFilterKV.
trace
Logs a message at the trace level.
warn
Logs a message at the warn level.

Structs§

Builder
Global config to setup logger See crate::recipe for usage
ByAge
Define the time to rotate files.
CaptainsLogLayer
An tracing-subscriber layer implementation, for capturing event from tracing
EnvVarDefault
An intermedium type for crate::env_or(), parsing environment with default values.
FormatRecord
For accessing log Record in crate::LogFormat
GlobalLogger
Global static structure to hold the logger
LogBufFile
Config for buffered file sink which merged I/O and delay flush. Optional log rotation can be configured.
LogConsole
Log config for output to console
LogFilter
A LogFilter supports concurrent control the log level. Use in combine with macros logger_XXX
LogFilterKV
LogFilter that carries one additional value into log format
LogFormat
Custom formatter which adds into a log sink
LogRawFile
Config for file sink that supports atomic append from multiprocess.
LogRingFileringfile
The LogRingFile sink is a way to minimize the cost of logging, for debugging deadlock or race condition, when the problem cannot be reproduce with ordinary log (because disk I/O will slow down the execution and prevent the bug to occur).
Rotation
Log rotation configuration.
Syslogsyslog
Config for syslog output, supports local and remote server.

Enums§

Age
ConsoleTarget
Facility
Level
Re-export log::Level: An enum representing the available verbosity levels of the logger.
LevelFilter
Re-export log::LevelFilter: An enum representing the available verbosity level filters of the logger.
SyslogAddrsyslog
SyslogProtosyslog
Upkeep

Traits§

SinkConfigTrait

Functions§

env_or
To config some logger setting with env.
get_global_logger
Return the GlobalLogger after initialized.
setup_log
Initialize global logger from Builder

Type Aliases§

FormatFunc

Attribute Macros§

logfn
Provide an proc_macro #[logfn] which log the information: