Structured Logging in Rust using Slog

Add structured logs to your Rust application using Slog crate

Slog is a structured logging library for rust-lang.

Structured logging is a concept that puts events over messages, events are logged with associated key-value data, not plain string messages. This helps in:

  • processing log files for analytics
  • searching and debugging (example: search log statements for particular user or request)

Basic Logging with Slog

Lets create a simple logger:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#[macro_use]
extern crate slog;
extern crate chrono;

fn main() {
    let drain = slog::Discard;

    let root_logger = slog::Logger::root(drain, o!());

    info!(root_logger, "Application started";
        "started_at" => format!("{}", chrono::Utc::now()));
}

Important terms:

  • Drain is a trait which is responsible for deciding the output/destination of the logs. Any custom log handling logic should be implemented as a Drain.
  • Context are key-value pairs that gets added into every log statement. Let’s say if you want to add the git commit version to each log statement you can add it here.
  • Logger are objects used to execute logging statements.
    For creating root logger, we need drain and context. Read complete documentation here.

In above example, a root_logger object is created with drain Discard (which discards everything) and blank context (o!()).


Lets look at another example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#[macro_use]
extern crate slog;
extern crate slog_term;
extern crate chrono;

use slog::Drain;

fn main() {
    let decorator = slog_term::TermDecorator::new().build();
    let drain = slog_term::FullFormat::new(decorator).build().fuse();

    let root_logger = slog::Logger::root(drain, o!());

    info!(root_logger, "Application started";
        "started_at" => format!("{}", chrono::Utc::now()));
}

Here, slog_term::FullFormat drain is used for terminal output with TermDecorator (Decorator is an implementing strategy of output formating in terms of IO, colors, etc.)

Customizable Drains

Since drain is a trait to customize logging to any output, it allows slog to become extensible, composable, and flexible. There are many feature libraries based on this trait:

You can always write your own implementation using this trait.

Multiple Drains

Drains can also be interlinked and extended according to our needs. Following is an example where there are two outputs:

  • to stdout, where all log statements are sent
  • to stderr, where log statements with log level greater than or equal to slog::Level::Warning are sent
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#[macro_use]
extern crate slog;
extern crate slog_term;
extern crate chrono;

use slog::Drain;

fn main() {
    let drain = slog_async::Async::new(
        slog::Duplicate::new(
            slog::Filter::new(
                slog_term::FullFormat::new(
                    slog_term::PlainSyncDecorator::new(std::io::stderr(),)).build(),
                |record: &slog::Record| record.level().is_at_least(slog::Level::Warning),
            ),
            slog_term::FullFormat::new(slog_term::PlainSyncDecorator::new(std::io::stdout())).build(),
        ).fuse()
    ).build().fuse();

    let root_logger = slog::Logger::root(drain, o!());

    info!(root_logger, "Application started";
        "started_at" => format!("{}", chrono::Utc::now()));
}

Here, Async, Duplicate, Filter, FullFormat drains are used.

Log Macros and Log Levels

To log a statement/record, we use log macros. In the above examples, info! is used.

Log Levels are log categories based on urgency. For example, we have debug!, info!, error!, etc. for self-explaining urgencies. We have different log macros for different log levels.

Extra

You can find many more examples here.