Skip to content

Introduction to structured logging with slog

vtta edited this page Feb 21, 2020 · 8 revisions

Overview

slog separates concepts of creating logging information (eg. info!(log, "aborting operation")) from actually handling log records ("writing them down somewhere" - eg. to a file). For log record creation slog provides Loggers that can form hierarchies. For log record handling the concept of Drains (logging "outputs") is introduced.

Drains

Since usually only the end application itself, knows the details on which logs, when, and where should be stored, root Drain is usually created somewhere early in the main function.

slog's Drains are simply structs implementing a Drain trait.

Let's take a look at the simplest Drain: a slog::Discard . It's a drain that simply drops any logging records that are send to it. Because of that, it can never return any error: type Error = Never.

A more interesting Drain is a slog::LevelFilter. It's a generic struct that wraps another drain (D : Drain), and forwards to it only logging Records that have logging level equal or higher than a given value. Because the underlying drain can potentially return an error, LevelFilter will return such error (type Error = D::Error).

LevelFilter shows another benefit of slog Drains - they are composable. Due to Rust generic system, multiple drains can be combined together, and compiled to very efficient code, with endless possibilities for writing new Drains providing new features.

A root Drain is a drain that never returns any Errors (type Error = Never). It's an important distinction since Loggers can only be build on top of Drains that can't return an error. Depending on the application requirements, a slog::Fuse or slog::IgnoreErr can typically be used to either panic on errors, or ignore them completely. More sophisticated custom strategies like fallback, etc. can easily be implemented, with most useful ones possibly added to standard slog or one of associated crates.

Libraries typically should not build their own drains, and instead rely on Loggers provided by the library user. A typical log crate-backward-compatibile approach has been documented in slog-using example library.

Let's take a look at the example drain hierarchy:

let file = File::create("/tmp/myloggingfile").unwrap();
let stream = slog_stream::stream(file, slog_json::new().build());
let syslog = slog_syslog::unix_3164(slog_syslog::Facility::LOG_DAEMON);
let root = Logger::root(Duplicate::new(
        LevelFilter::new(stream, Level::Info),
        LevelFilter::new(syslog, Level::Warning),
).fuse(), o!());

from an drain-graph.rs example

The graph illustrating the drain-hierarchy created:

slog drain hierarchy example

Loggers

In slog any logging statement requires a Logger object.

The first Logger created will always have to be a root Logger using slog::Logger::root. Any other Logger objects can be build from the existing ones as it's child, using slog::Logger::new.

Each Logger has a list of key-value pairs associated with it. Each time a child Logger is created it inherits all the pairs from its parent. These allows building a contextual information, that corresponds to the logical execution-level structure of the application, as opposed to the structure of the code itself.

Eg. An application has it's own context data like:

  • time it was build and revision of the code used
  • time when it was started

Then inside application there might be different components like:

  • web server with information on:
    • the ip and port it is listening on
  • (multiple) job processing threads each with:
    • directory it's working on

A web server handles request from multiple peers, each described by:

  • user id
  • IP

A job processing handles different jobs described by:

  • user id
  • file
  • type

For each of the above components, typically a new Logger object would be created, adding more information to the parent-component. So then, when an error case is handled, logging can be just:

error!(job.logger, "write failed"; 'error' => error);

And that would cause application to log a message containing, both the error itself, and information about it's logical runtime context: which file, what type of a job, for which user, and so on.

Crates

TBD