-
Notifications
You must be signed in to change notification settings - Fork 95
Introduction to structured logging with slog
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 Logger
s that can form hierarchies. For log record handling the concept of Drain
s (logging "outputs") is introduced.
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 Record
s 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
Drain
s - 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 Drain
s providing new features.
A root Drain
is a drain that never returns any Errors (type Error = Never
). It's an important distinction since Logger
s can only be build on top of Drain
s 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 Logger
s 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:
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.
TBD