Description
There are a couple of distinct kinds of programs, each of which has different needs from a logger:
- Short running programs like
rustc
need a simple logging framework that provides some reasonable level of configurable filtering when trying to debug a problem. - Embedded programs or kernels may not have a
stderr
at all. They may, however, have the ability to log information through a serial port or network interface. - Long running programs like servers have more advanced logging needs than the current crate provides. In this case, dynamic reconfiguration of logging filters is very helpful, as is the ability to have multiple "sinks" that can output to the console, rolling files, the syslog, over the network, etc.
- Programs that don't care about logging shouldn't be spammed with unwanted information from libraries that they use.
The current liblog implementation covers the first and last use cases, but misses the middle two.
There are two competing concerns here: we want a single logging interface that libraries can use - it would be crazy to expect someone to provide different versions of a single crate that only differ by their choice of logging framework! But at the same time, we want the libraries to be usable in all of the contexts mentioned above, with their logging intact.
One solution is to turn liblog
into a logging "facade". It provides the infrastructure to allow libraries to send log messages, while allowing the downstream application to chose the actual logging framework that will process the log messages. One example is the slf4j library for Java, which allows libraries to log in a way that will work with any of the many logging frameworks available in Java: java.util.logging, log4j, Logback, etc.
I have a prototype conversion of liblog to a facade in the log-ng project. It consists of three crates:
log_ng
- the core facadebasic
- a port of liblog's currentRUST_LOG
based log implementationlog4r
- a prototype of a more complex logger in the vein of log4j and Logback.
Documentation is available here: http://sfackler.github.io/log-ng/doc/log_ng/
Changes from liblog
While converting liblog
, I did make some changes to the current functionality:
- The log levels are now an enum instead of a
u32
, consisting ofOff
,Error
,Warn
,Info
,Debug
, andTrace
. Note thatTrace
is new. I don't really see the use in having 255 distinct levels, and it simplifies parsing and pretty-printing of the level if we can restrict the level to an enum. - The logging macros don't call
enabled
before callinglog
. It turned out when I was experimenting with logger implementations thatlog
can pretty trivially perform the filtering itself. If argument computation is expensive, thelog_enabled!
macro is still available, which will call through toenabled
. - There is now a single global logger instead of thread-local loggers. The current
liblog
implementation does provide the ability to set the logger, but since it's task local, it's impossible to make sure the right logger is installed in each thread - think of threads started byTaskPool
for example. Having thread-local loggers also results in a log of duplicated data and computation. TheRUST_LOG
environment variable has to be reparsed every time a program logs on a new thread, for example. In addition, it's not totally clear to me that anyone actually wants to have different loggers on different threads. Even if one does, it's pretty easy to make a thread local logger implementation for the logging facade. - The
ndebug
cfg is checked only indebug!
andtrace!
, not inlog!
as well. I'm not sure what the right thing to do is here, but it seems like completely forbidding the use of the debug and trace levels with optimizations enabled is over-aggressive, especially since the global maximum log level check makes them pretty cheap when those levels are disabled. We may want to havedebug!
andtrace!
always enabled, and have separate debug build only versions.
Interesting notes/design decisions
- The logger may only be initialized once, and isn't torn down when a program exits. It's possible to support logger replacement and teardown with
atexit
but it would require storing the logger in anArc
and adding a couple extra atomic reads and writes every time a message is logged. I'm not sure what the right answer is here. I don't really think people will want to swap their loggers out dynamically, but cleanup at exit seems like a nice thing to do to avoid spamming Valgrind. - Log messages generated when the logger is not initialized are ignored. Some logging facades like slf4j fall back to a simple logging implementation that dumps messages at the higher levels to the console. We could do this, but I'm a bit hesitant to do something that'll clutter the console for programs that don't want logging information as well as slow them down.
- A logger has to be manually installed. slf4j can use some reflective classpath scanning magic to automatically set itself up to use common logging frameworks if they're present on the classpath, but we don't really have that ability. We may be able to do some dark magick with weak linkage, but I think that explicit is better than implicit here.
Remaining problems/blockers
- Since the logger has to be manually initialized, it becomes very hard to use when running tests. You could make a Once and use that to initialize the logger in every test, but that adds a huge amount of pain to adding new tests. I think the best solution here is to augment rust's test support to add something like Go's
TestMain
function or junit's@BeforeClass
annotation to allow for global setup before any tests run. This is useful in other contexts as well for other global resource setup.
Thoughts?