Here I want to describe how I plan to refactor the logging back end implementation for imag.
What we have
Right now, the logging implementation is ridiculously simple. What we do is:
On every call to one of the logging macros, the
log crate gives us an object
with a few informations (line number, file, log message,…) - we apply our
format, some color and write it to
This is of course rather simple and not really flexible.
What we want to have
I want to rewrite the logging backend to give the user more power about the
logging. As we only have to rewrite the backend, and the
log crate handles
everything else, the actual logging looks non different and “client” code does
+----------------------------+ | imag code, libs, bins, ... | +----------------------------+ | | calls | v +----------------------------+ | crate: "log" | +----------------------------+ | | calls | v +----------------------------+ | imag logging backend impl. | +----------------------------+
So what features do we want? First of all, the imag user must be able to configure the logging. Not only with the configuration file but also via environment variables and of course command line parameters. The former will be overridden by the latter, respectively. This gives the user nice control, as she can configure imag to log to stderr with only warnings being logged but when calling a script of imag commands or calling imag directly from the command line, these settings can be temporarily (for the script or one command) be overridden.
The configuration options I have in mind are best described by an example:
# The logging section of the configuration [logging] # the default logging level # Valid values are "trace", "debug", "info", "warn", "error" level = "debug" # the destinations of the logging output. # "-" is for stderr, multiple destinations are possible default-destinations = [ "-", "/tmp/imag.log" ] # The format for the logging output # # The format supports variables which are inserted for each logging call: # # "%no%" - The of the logging call # "%thread%" - The thread id from the thread calling the log # "%level%" - The logging level # "%module%" - The module name # "%file%" - The file path where the logging call appeared # "%line%" - The line No of the logging call # "%message%"" - The logging message # # Functions can be applied to the variables to change the color of # the substitutions. # # A format _must_ contain "%message%, else imag fails because no logging should # be forbidden # [logging.formats] trace = "cyan([imag][%no%][%thread%][%level%][%module%][%file%][%line%]): %message%" debug = "cyan([imag][%no%][%thread%][%level%][%module%][%file%][%line%]): %message%" info = "[imag]: %message%" warn = "red([imag]:) %message%" error = "red(blinking([imag][uppercase(%level%)]): %message%)" # Example entry for one imag module # If a module is not configured or keys are missing # the default values from above are applied [logging.modules.libimagstore] enabled = true level = "trace" destinations = [ "-" ] # A format is only globally configurable, not per-module
One of the most complex things in here would be the format parsing, as variable expansion and functions to apply are some kind of DSL I have to implement. I hope I can do this - maybe there’s even a crate for helping me with this? Maybe the shellexpand library will do?
These things and configuration options give the user great power over the logging.
Because imag already logs a lot, I think about an approach where one thread is used for the actual logging. Because each logging call involves a lot of complexity, I want to move that to a dedicated thread where other threads speak to the logging thread via a MPSC queue.
Of course, this should be opt-in.
The idea is that the logging starts a thread upon construction (which is really
early in the imag process, nearly one of the first operations done). This
happens when the
Runtime object is build and hence no “client code” has to be
changed, all changes remain in libimagrt.
This thread is bound to the
Runtime object, logging calls (via the logging
backend which is implemented for the
log crate) talk to it via a channel. The
thread then does the heavy lifting. Of course, configuration can be aggregated
on construction of the logging thread.
The logging thread is killed when the
Runtime object is dropped (one of the
last operations in each imag process). Of course, the queue has to be emptied
before the logging is closed.
I am also thinking about converting the code base to use the
slog crate, which
offers structured logging. But I’m not yet sure whether we will benefit from
that, because I don’t know whether we would need to pass a state-object around.
If that is the case, I cannot do this as this would introduce a lot of
complexity which I don’t want to have. If no such object needs to be passed
around, I still have to evaluate whether the
slog crate is a nice idea and of
course this would also increase the number of (complex) dependencies by one…
and I’m not sure whether the benefits outrule the inconveniences.