Summary

This RFC introduces a logging library - log - and some recommendations on how to log in the Bee project.

Motivation

Logging is done across almost all binary and library crates in the Bee project, so consistency is needed to provide a better user experience.

Detailed design

Logging in Bee should be done through the log crate (A Rust library providing a lightweight logging facade) which is the de-facto standard library for logging in the Rust ecosystem.

The log crate itself is just a frontend and supports the implementation of many different backends.

It provides a single logging API that abstracts over the actual logging implementation. Libraries can use the logging API provided by this crate, and the consumer of those libraries can choose the logging implementation that is most suitable for its use case.

Frontend

Libraries should link only to the log crate, and use the provided macros to log whatever information will be useful to downstream consumers.

The log crate provides the following macros, from lowest priority to highest priority:

  • trace!
  • debug!
  • info!
  • warn!
  • error!

These macros have a usage similar to the println! macro.

A log request consists of a target, a level, and a message.

By default, the target represents the location of the log request, but may be overridden.

Example with default target:


#![allow(unused)]
fn main() {
info!("Connected to port {} at {} Mb/s", conn_info.port, conn_info.speed);
}

Example with overridden target:


#![allow(unused)]
fn main() {
info!(target: "connection_events", "Successful connection, port: {}, speed: {}", conn_info.port, conn_info.speed);
}

Backend

Executables should choose a logging implementation and initialize it early in the runtime of the program. Logging implementations will typically include a function to do this. Any log messages generated before the implementation is initialized will be ignored.

There are a lot of available backends for the log frontend.

This RFC opts for the fern backend which is a very complete one with an advanced configuration.

Initialization

The backend API is limited to one function call to initialize the logger. It is designed to be updatable without breaking changes by taking a LoggerConfig configuration object and returning a Result.


#![allow(unused)]
fn main() {
fn logger_init(config: LoggerConfig) -> Result<(), Error>;
}

Configuration

The following configuration - compliant with RFC 31 - primarily allows configuring different outputs with different log levels.

config.rs


#![allow(unused)]
fn main() {
#[derive(Clone)]
struct LoggerOutputConfig {
    name: String,
    level: LevelFilter,
    ...
}

#[derive(Clone)]
struct LoggerConfig {
    color: bool,
    outputs: Vec<LoggerOutputConfig>,
    ...
}
}

config.toml

[logger]
color = true
[[logger.outputs]]
name  = "stdout"
level = "info"
[[logger.outputs]]
name  = "errors.log"
level = "error"

Note: stdout is a reserved name for the standard output of the console.

Errors

Since different backends may have different errors, we need to abstract them to provide a consistent logger_init function.


#![allow(unused)]
fn main() {
#[derive(Debug)]
#[non_exhaustive]
enum Error {
    ...
}
}

Note: The Error enum has to be non_exhaustive to allow further improving / extending the logger without risking a breaking change.

Format

The following elements should appear in this order:

  • Date in the %Y-%m-%d format e.g. 2020-05-25;
  • Time in the %H:%M:%S format e.g. 10:23:03;
  • Target e.g. bee_node::node;
    • The default target is preferred but can be overridden if needed;
  • Level e.g. INFO;
  • Message e.g. Initializing...;

All elements should be enclosed in brackets [...] with only a space between the level and the message.

Example: [2020-05-25][10:23:03][bee_node::node][INFO] Initializing...

Note: All messages should end either with ... (3 dots) to indicate something potentially long-lasting is happening, or with . (1 dot) to indicate events that just happened.

Drawbacks

No specific drawbacks using this library.

Rationale and alternatives

  • The log crate is maintained by the rust team, so it is a widely used and trusted dependency in the Bee framework;
  • Most of the logging crates in the Rust ecosystem are actually backends for the log crate with different features;
  • It should be very easy to switch to a different backend in the future by only changing the initialization function;
  • There are a lot of available backends but fern offers a very fine-grained logging configuration;

Unresolved questions

There are no open questions as this RFC is pretty straightforward and the topic of logging is not a complex one.