12 releases (6 breaking)

0.7.1 Jun 8, 2023
0.6.1 May 14, 2023
0.6.0 Sep 29, 2019
0.3.0 Jul 20, 2019

#421 in Concurrency

Download history 98/week @ 2024-07-28 15/week @ 2024-09-22 2/week @ 2024-09-29

70 downloads per month
Used in vxdraw

LGPL-3.0-or-later

73KB
1.5K SLoC

build status Latest version Documentation

Fast logger

Fast-logger is a logger for Rust that attempts to be the fastest (with the lowest caller-latency time) logger for rust. It achieves this by not performing dynamic allocations and passing formatting data over a channel to another thread. The reason for doing this is that formatting itself is an expensive operation.


lib.rs:

A simple log-level based logger

General

When spawning a logger, a thread is created and a handle structure returned. This handle can be copied and shared among threads. All it does is hold some mutexes and atomic references to the logger. Actually logging pushes the data over an asynchronous channel with size limits.

The logger requires a Display type to be provided so the logger can actually print data. The reason for this is that it cuts down on serialization cost for the caller, leaving the logger to serialize numbers into strings and perform other formatting work.

Compatibility mode

There are many loggers out there in the wild, and different libraries may use different loggers. To allow program developers to log from different sources without agreeing on a logger to use, one can interface with [Compatibility]. Logging macros work with [Compatibility].

Log Levels

Logger features two log level controls: per-context and "global" (Note: The logger has no globals, everything is local to the logger object). When logging a message, the global log level is checked, and if the current message has a lower priority than the global log level, nothing will be sent to the logger.

Once the logger has received the message, it checks its internal mapping from context to log level, if the message's log level has a lower priority than the context log level, it is dropped.

We normally use the helper functions trace, debug, info, warn, and error, which have the corresponding log levels: 255, 192, 128, 64, 0, respectively.

Trace is disabled with debug_assertions off.

Note: an error message has priority 0, and log levels are always unsigned, so an error message can never be filtered.

Example - Generic

Note that generic logging requires indirection at runtime, and may slow down your program. Still, generic logging is very desirable because it is easy to use. There are two ways to do generic logging depending on your needs:

use fast_logger::{info, Generic, Logger};

fn main() {
    let logger = Logger::<Generic>::spawn("context");
    info!(logger, "Message {}", "More"; "key" => "value", "three" => 3);
}

The other macros are [trace!], [debug!], [warn!], [error!], and the generic [log!].

If you wish to mix this with static logging, you can do the following:

use fast_logger::{info, Generic, Logger};

enum MyMsg {
    Static(&'static str),
    Dynamic(Generic),
}

impl std::fmt::Display for MyMsg {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            MyMsg::Static(string) => write!(f, "{}", string),
            MyMsg::Dynamic(handle) => handle.fmt(f),
        }
    }
}

impl From<Generic> for MyMsg {
    fn from(f: Generic) -> Self {
        MyMsg::Dynamic(f)
    }
}

fn main() {
    // Setup
    let logger = Logger::<MyMsg>::spawn("context");
    info!(logger, "Message {}", "More"; "key" => "value", "three" => 3);
}

Example of static logging

Here is an example of static-logging only, the macros do not work for this, as these generate a [Generic]. Anything implementing [Into] for the type of the logger will be accepted into the logging functions. This is the fast way to log, as no [Box] is used.

use fast_logger::Logger;

// You need to define your own message type
enum MyMessageEnum {
    SimpleMessage(&'static str)
}

// It needs to implement std::fmt::Display
impl std::fmt::Display for MyMessageEnum {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            MyMessageEnum::SimpleMessage(string) => write!(f, "{}", string),
        }
    }
}

fn main() {
    // Setup
    let logger = Logger::<MyMessageEnum>::spawn("ctx");

    // Actual logging
    logger.info(MyMessageEnum::SimpleMessage("Hello world!"));

    // Various logging levels
    logger.trace(MyMessageEnum::SimpleMessage("Hello world!"));
    logger.debug(MyMessageEnum::SimpleMessage("Hello world!"));
    logger.info(MyMessageEnum::SimpleMessage("Hello world!"));
    logger.warn(MyMessageEnum::SimpleMessage("Hello world!"));
    logger.error(MyMessageEnum::SimpleMessage("Hello world!"));
}

Example with log levels

Here is an example where we set a context specific log level.

use fast_logger::Logger;

// You need to define your own message type
enum MyMessageEnum {
    SimpleMessage(&'static str)
}

// It needs to implement std::fmt::Display
impl std::fmt::Display for MyMessageEnum {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            MyMessageEnum::SimpleMessage(string) => write!(f, "{}", string),
        }
    }
}

fn main() {
    // Setup
    let logger = Logger::<MyMessageEnum>::spawn("ctx");

    // Set the log level of `ctx` to 70, this filters
    // All future log levels 71-255 out.
    assert!(logger.set_context_specific_log_level("ctx", 70));

    // This gets printed, because `warn` logs at level 64 <= 70
    logger.warn(MyMessageEnum::SimpleMessage("1"));

    // This gets printed, because 50 <= 70
    logger.log(50, MyMessageEnum::SimpleMessage("2"));

    // This does not get printed, because !(80 <= 70)
    logger.log(80, MyMessageEnum::SimpleMessage("3"));

    // This gets printed, because the context is different
    logger.clone_with_context("ctx*").log(128, MyMessageEnum::SimpleMessage("4"));
}

Example with just strings

If you really don't care about formatting overhead on the caller's side, you can just use a [String] as the message type.

use fast_logger::Logger;

fn main() {
    // Setup
    let logger = Logger::<String>::spawn("ctx");

    // Set the log level of `ctx` to 70, this filters
    // All future log levels 71-255 out.
    assert!(logger.set_context_specific_log_level("ctx", 70));

    // This gets printed, because `warn` logs at level 64 <= 70
    logger.warn(format!("1"));

    // This gets printed, because 50 <= 70
    logger.log(50, format!("2"));

    // This does not get printed, because !(80 <= 70)
    logger.log(80, format!("3"));

    // This gets printed, because the context is different
    logger.clone_with_context("ctx*").log(128, format!("4"));
}

Non-Copy Data

Data that is non-copy may be hard to pass to the logger, to alleviate this, there's a builtin clone directive in the macros:

use fast_logger::{info, Generic, InDebug, Logger};

#[derive(Clone, Debug)]
struct MyStruct();

fn main() {
    let logger = Logger::<Generic>::spawn("context");
    let my_struct = MyStruct();
    info!(logger, "Message {}", "More"; "key" => InDebug(&my_struct); clone
    my_struct);
    info!(logger, "Message {}", "More"; "key" => InDebug(&my_struct));
}

Nested Logging

It is sometimes useful to nest logging contexts, here's how to do that:

use fast_logger::{info, Generic, Logger};

fn main() {
    let logger = Logger::<Generic>::spawn("context");
    let submodule = logger.clone_add_context("submodule");
    let mut something = submodule.clone_add_context("something");
    info!(something, "This message appears in the context: context-submodule-something");
}

Dependencies

~1–11MB
~65K SLoC