Some ways to log stuff in java
Posted 2013-11-14 15:10. Tagged java, development, debugging.
Logging in java is a mess. For a long time there was no standard way of logging in java, so there is a lot of 3:rd party solutions. Apache commons logging, slf4j, and log4j are probably the most used.
By now, there is standard java.util.logging
package, but most people
stick to the old 3:rd party solutions.
As we shall see, that isn’t really surprising, since java util logging
kind of sucks.
I have done some exploration of different options for logging in java.
The code I base this text on is available as kaj/explorelogs on github. I explore the industry-standard logging packages slf4j, log4j, and appache commons logging as well as java.util.logging. And I take a look at the Loggger in play framework 1.x as well as my own wrapper around log4j.
API and configuration
When selecting what logger to use, two things are important. The API and the performance. For the performance it is important that a logging a message should be reasonably fast, but I think it is even more important that not logging a filtered message should be extremely fast, so the cost of extra debug messages should be close to zero in a production environment where debug logging is turned of.
A relevant part of the running time of logging a message might be composing the message by converting some objects to strings and concatenating it all together, so I think a good logging API should make that happen only for actually used messages. Even more importantly, it should be easy and failsafe to get and use a logger.
A simple wrapper
private static final LogWrapper logger ;
logger.;
This is how I think logging should be done. Initializing is one single import and a single static call. I almost always want the category of a logger to be the fully qualified classname of the class logging to it, and since java has no way of saying “this class” statically, I have a named constructor to find out the calling class.
Actually logging a message has the full power and convenience of
String.format
.
The wrapper uses log4j for the actual logging, so see below for
configuration.
The only thing wrong with this is creating yet another logging package for java.
slf4j
private static final Logger logger ;
logger.;
Two imports just to get the logger? Ok, I can live with that, I guess.
There is message formatting, so I don’t have to build the string for
unused log messages, but there is only one way to format, the n:th
{}
is replaced with String.valueOf(
the n:th argument)
, so no
formatting for numbers or dates or other formatable classes.
Slf4j can use various destination implementations.
The default configuration is to use the first destination class it can
find on the classpath, so just add slf4j-foo.jar
to log to foo.
Java util logging
final Logger logger ;
final Logger logger_alt ;
logger.;
logger.;
Initialization is almost as good as slf4j, but I normally prefer to
get a logger for a class rather than a string.
I could use the name of a class as a string constant, but that is very
prune to errors when refactoring.
I find the risk of using the wrong class is big enough even for the
MyClass.class.getName()
approach, but that is the same for all
implementations except my own wrapper.
Actual logging is somewhat painful.
There are methods like logger.debug(msg)
and logger.info(msg)
, but
they just take a string argument, and not a format string with
arguments.
The log
method which does take a format and arguments for some
reason do not accept a varargs parameter, so an explicit array must be
created (there is a method for just one arg, but not two or more).
There doesn’t seem to be a default file name for configuring java util
logging from the classpath.
Instead, the system property java.util.logging.config.file
needs to
be set to a file name (absolute or relative to current working
directory, not from classpath) for any configuration to be loaded.
Otherwise, the default is to log to System.err
in a two-line format
with American time-stamps.
log4j
private static final Logger logger ;
logger.;
if
Initialization is simple enough.
Log4j does not provide any formatting, so we have to build the string
manually.
If we know the message will rarely be actually logged and one of the
toString() methods is slow (or the strings are particularly large,
putting extra load on the garbage collector), we can query
isDebugEnabled()
and only log the message when meaningful.
But that means a three-line block for a simple debug log, which is
probably not gonna happen (so I don’t do it in my test code).
The default place for configuration is a log4j.properties
(or xml)
on the load-path.
Apache commons
private static final Log logger ;
logger.;
if
Very similar to log4j, except an extra factory class is needed for initialization. The main problem is the potentially expensive message building for ignored messages, just as for log4j. The same workaround is possible.
Apache commons logging has support for multiple backends. The most commonly used backend is probably log4j, and that is used automatically if available.
Play framework 1.2
Logger.;
At first glance, this is very convenient.
You don’t even have to have a logger object, just call static debug /
info / warn / error methods on the Logger class.
And you get the full power of String.format
for the messages.
The downside is that there are no separate loggers for different classes, so you can’t use different level filters for different parts of the project and you don’t know what class created a specific message. (Actually, the play logger can record the caller, but that gets very expensive in runtime. I turn this off in my test code).
Performance
To measure the performance of each kind of logger, I wrote a class with a method like this for each logger:
private long
The exact way of logging an info and a debug message varies with the API for the kind of logger used,
see source for details
.
The LazyFib
object uses a naïve calculating of the (i % MAX_FIB)
:th fibonacci number as a simple example of a toString
method doing something slow.
First, here’s timing stats for what I think is a rather normal usage, with fifty ignored debug messages for each saved info message.
slf4j: 83 ms for 1000 logged and 50000 filtered messages.
my wrap: 117 ms for 1000 logged and 50000 filtered messages.
Log4j: 184 ms for 1000 logged and 50000 filtered messages.
Commons: 189 ms for 1000 logged and 50000 filtered messages.
java.util: 322 ms for 1000 logged and 50000 filtered messages.
Play: 591 ms for 1000 logged and 50000 filtered messages.
Of course, sometimes the wast majority of messages are ignored, in which case the only important thing is to ignore junk messages fast, here even java.util almost shines:
slf4j: 318 ms for 1000 logged and 10000000 filtered messages.
my wrap: 395 ms for 1000 logged and 10000000 filtered messages.
Log4j: 27142 ms for 1000 logged and 10000000 filtered messages.
Commons: 27183 ms for 1000 logged and 10000000 filtered messages.
java.util: 541 ms for 1000 logged and 10000000 filtered messages.
Play: 43684 ms for 1000 logged and 10000000 filtered messages.
But what’s up with play here?
Since it has formatting, we would expect ignored messages to be fast,
but it is by far the slowest logger in this setup.
Indeed, looking in the play source for Logger.debug()
we find:
log4j.;
So here we have the full cost of String.format
formatting (with some
extra overhead) even for ignored messages.
No wonder it is slow.
For symmetry, here’s a run with no filtered messages at all. Now the nice formatting in my wrap comes at full cost (and the extra stupidity of play doesn’t matter). I don’t know what java.util does to be so slow:
slf4j: 352 ms for 10000 logged and 0 filtered messages.
my wrap: 547 ms for 10000 logged and 0 filtered messages.
Log4j: 242 ms for 10000 logged and 0 filtered messages.
Commons: 243 ms for 10000 logged and 0 filtered messages.
java.util: 1027 ms for 10000 logged and 0 filtered messages.
Play: 529 ms for 10000 logged and 0 filtered messages.
All of the above was with one object with a rather slow toString
method in the message (MAX_FIB = 20
in the test code).
So, just for fairness, here’s a sample with MAX_FIB=1
.
slf4j: 82 ms for 1000 logged and 50000 filtered messages.
my wrap: 115 ms for 1000 logged and 50000 filtered messages.
Log4j: 84 ms for 1000 logged and 50000 filtered messages.
Commons: 85 ms for 1000 logged and 50000 filtered messages.
java.util: 311 ms for 1000 logged and 50000 filtered messages.
Play: 458 ms for 1000 logged and 50000 filtered messages.
slf4j: 298 ms for 1000 logged and 10000000 filtered messages.
my wrap: 366 ms for 1000 logged and 10000000 filtered messages.
Log4j: 1961 ms for 1000 logged and 10000000 filtered messages.
Commons: 2132 ms for 1000 logged and 10000000 filtered messages.
java.util: 593 ms for 1000 logged and 10000000 filtered messages.
Play: 17980 ms for 1000 logged and 10000000 filtered messages.
As you can see, slf4j still takes the lead, even if log4j or commons isn’t far behind when there is few ignored messages.
And for symmetry, here’s a run with MAX_FIB = 35
.
The most common log messages in a real project won’t be this slow, but
in a large project it is not unlikely that some toString
methods
will be this slow, and I wouldn’t presume to known which ones …
slf4j: 159 ms for 100 logged and 10000 filtered messages.
my wrap: 166 ms for 100 logged and 10000 filtered messages.
Log4j: 14149 ms for 100 logged and 10000 filtered messages.
Commons: 14138 ms for 100 logged and 10000 filtered messages.
java.util: 210 ms for 100 logged and 10000 filtered messages.
Play: 14327 ms for 100 logged and 10000 filtered messages.
Ok, enough of numbers. Feel free to grab the code and vary the parameters. If you add any other loggers to the test, I would apreciate hearing about it.
Conclusion
From now on, I will use slf4j for logging whenever I use java and have
the power to choose.
And any code that logs "foo: " + foo
for a potentially large object
foo
, as well as any mention at all of java.util.logging
or
play.Logger
, is reason to be careful.
I wont use my wrapper in any real code.
Even though slf4j doesn’t provide the full power of String.format
, I
think the simple formatting it has is enough to avoid using and
spreading yet another competing api.
Comments
This post is 10 years old, comments are disabled.