Context
Open Analytics is working on a huge data processing pipeline in the context of high-content imaging. The latest work to this end is a new product that will work in conjunction with Phaedra as part of the pipeline which now covers much more ground than before. The entire backend of this new component is written in Rust and consists of several web servers.
Proper log rotation has been a focus as of late, since it is critical to be
able to keep on top of things in this massive processing pipeline. In our Rust
web servers, we use slog
with
slog-json
for structured logging,
writing log records to file as JSON objects. Log rotation is enabled through the
file-rotate crate which plugs in as a
middleware wrapping the
File to which log records
are written.
This crate is essentially like having logrotate
embedded in each application
binary. The benefit to this is that no infrastructure work is required in order
to set up log rotation. Unlike logrotate
, instead of a periodic task, the
rotation logic is executed whenever log records are written to file.
file-rotate
does not necessarily aim to be exactly like the classic
logrotate
, but provides some useful features, like:
- Content limit configuration: when to rotate
- Using count or timestamp as suffix
- Age-based deletion of log files, or a maximum number of files
- Optional compression
- Getting a list of log files
The benefit of the last point will become clear soon.
In our setup, we create files with log rotation as follows:
use file_rotate::{
compression::Compression,
suffix::{FileLimit, TimestampSuffixScheme},
ContentLimit, FileRotate,
};
pub fn file_rotate(path: PathBuf, lifetime: Duration) -> FileRotate<TimestampSuffixScheme> {
FileRotate::new(
path,
TimestampSuffixScheme::default(FileLimit::Age(lifetime)),
ContentLimit::BytesSurpassed(5_000_000),
Compression::OnRotate(2),
)
}
This creates a configuration of
FileRotate
which rotates files as they reach 5 MB, uses a timestamp as suffix, deletes
files as their timestamp reach a certain age, and compresses all but the two
most recent log files.
For example, these are some files found in production:
all.log
all.log.20220203T081449
all.log.20220203T071848
all.log.20220203T062250.gz
all.log.20220203T052652.gz
Read more about the library’s API in the file-rotate
rustdocs.
Enter assemble-logs
: Tool to assemble the contents of rotated log files, with filtering and pretty formatting
Now that our log history is scattered across multiple files, some of which are compressed, it can be impractically difficult to unravel the story of what happened in our application by looking back in the log history.
A first instinct could be to write a script to assemble these logs for inspection. For example to concatenate the aforemnetioned 5 log files, we can write
$ gunzip -c all.log.20220203T052652.gz ; gunzip -c all.log.20220203T062250.gz ; cat all.log.20220203T071848 all.log.20220203T081449 all.log | less
A script would make things a little easier, but maybe we can do better by
utilizing the JSON format of log records to also do filtering and formatting. At
Open Analytics, we created a Rust binary crate that uses the same
file-rotate
library to do all of this:
It assumes that log records are written as JSON, and
- decompresses compressed log files
- assembles the contents of all log files in chronological order
- filters each record with an optional
jq
filter - formats the log records prettily with colors
- writes the formatted log records to stdout, so piping it to
less -r
is a good idea
Usage is as follows:
$ assemble-logs assemble -h
assemble-logs-assemble
USAGE:
assemble-logs assemble [FLAGS] [OPTIONS] <LOG_PATH> [JQ]
ARGS:
<LOG_PATH> The path to the main log file
<JQ> JQ query; must return a bool; only used for filtering
FLAGS:
-c, --compact Compact - don't print newline on each key-value
-h, --help Print help information
-n, --no-format No formatting: just print json
-V, --version Print version information
OPTIONS:
--jq-transformation <JQ_TRANSFORMATION> Only effective with --no-format
Filtering with jq
The <JQ>
arg is a jq
query that acts as a
log record filter and must return a boolean. Some examples:
Here is a full command line that would only show log records during a specific minute
assemble-logs assemble logs/all.log '.ts > "2021-09-02T22:30" and .ts < "2021-09-02T22:31"' | less -r
In the following we will only show the filter itself. Filter by (short) log
level (CRIT
, ERRO
, WARN
, INFO
, DEBG
, TRCE
):
'.level == "WARN" or level == "ERRO"'
Tags can be very useful, especially if your application has several independent components, and perhaps even asynchronous periodic tasks - we have plenty, so we frequently want to follow a specific tag. To show only records with a specific tag:
'.tag == "phaedra-poller"'
Everything actix-related (e.g. all incoming requests):
'.tag | startswith("actix")'
Filtering makes use of the JSON fields that
slog-json
outputs. An example JSON log
record:
{
"msg": "Start pollig worker",
"level": "DEBG",
"ts": "2022-01-31T15:20:20.637",
"tag": "poller",
"worker": "http://localhost:8002/",
"poller": 20166
}
The keys "msg", "level", "ts", "tag"
are always present. The rest of the keys
are key-value pairs set when logging with slog
. These can all be used in the
jq
filter, so there are plenty of possibilities.
Future work
In assemble-logs
, we plan to implement a follow
subcommand that acts like
tail -f
. It will of course include formatting and filtering just like the
assemble
subcommand. Another current shortcoming is that the suffix scheme is
hardcoded
(AppendTimestamp
)
- it should be possible to configure this, perhaps on the command line.
Summary
In our Rust projects we have opted for application-internal log rotation rather
than the conventional external log rotation using logrotate
. Everything is
inside the binary; log rotation is taken care of, even in your local dev setup.
Using a Rust library for log rotation has a further advantage: the library
provides a function to get a list of all log files, chronologically ordered,
with structured information about each suffix, which made the work on
assemble-logs
easier and less error prone (assuming that the library is
correct). Should some behaviour change in file-rotate
, we just have to update
our dependencies.