Tuesday, January 11, 2011

JDK Logging per Webapp

Logging in Java is probably the easier thing that has been made a lot complicated. In Java, you have plenty of logging libraries and wrappers amongst:
  • commons logging
  • log4j, log4j extras, jmx, mail
  • slf4j (api, implementation, commons logging wrapper, jdk logging wrapper, ...)
  • jdk logging (java.util.logging)
  • and a lot and lot of others
You aim to have in your web application at then end all of these stupid wrappers because libraries depends on them.
If you are concerned about performance, you already know that the best way to use those loggers are by testing then logging:

private static final Logger LOGGER = Logger.getLogger(MyClass.class.getName());
[...]
if (LOGGER.isLoggable(Level.FINE))
    LOGGER.fine("Creating new connection...");

My frustration when I see libraries and applications developed is always the same: You are provided by the JDK with a common logging system available to anyone without the need of any external JAR. You also write the same code when you want to log something.

So why the hell are people keep using libraries like SLF4J or commons-logging or even more !!!!

The JDK logging API, even if not as good as log4j, does everything you need. A shortcoming was that the JDK logging system is not well designed to work in an application server and have each web application controlling its own logging configuration. This is mainly due to the fact that java.* classes are directly loaded from the system classloader.

But this is not the case anymore !!!

I've written a very small JAR which you can put in your application server common libraries. This JAR enables your web applications to use the JDK logging system and have each different configurations.

You will find all information here: http://code.google.com/p/mycila/wiki/JdkLoggingPerWebapp

So now your don't need to use any logging wrapper anymore !! Just use the standard JDK Logging API.

But wait !!!

You can now develop libraries and code completely independent of any logging libraries. But what if someone wants to use the JDK API but still want to output logs with log4j (log rotation, compression, ...) ?

Writing a wrapper for the JDK Logging system is trivial ! Here is one that can redirect all JDK logging calls to log4j:

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Handler;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;

/**
 * @author Mathieu Carbou
 */
public final class JdkOverLog4j extends Handler {

    private static final Map<java.util.logging.Level, Level> LEVELS_JDK_TO_LOG4J = new HashMap<java.util.logging.Level, Level>() {
        {
            put(java.util.logging.Level.OFF, Level.OFF);
            put(java.util.logging.Level.SEVERE, Level.ERROR);
            put(java.util.logging.Level.WARNING, Level.WARN);
            put(java.util.logging.Level.INFO, Level.INFO);
            put(java.util.logging.Level.CONFIG, Level.INFO);
            put(java.util.logging.Level.FINE, Level.DEBUG);
            put(java.util.logging.Level.FINER, Level.DEBUG);
            put(java.util.logging.Level.FINEST, Level.DEBUG);
            put(java.util.logging.Level.ALL, Level.ALL);
        }
    };

    private static final Map<Level, java.util.logging.Level> LEVELS_LOG4J_TO_JDK = new HashMap<Level, java.util.logging.Level>() {
        {
            put(Level.OFF, java.util.logging.Level.OFF);
            put(Level.FATAL, java.util.logging.Level.SEVERE);
            put(Level.ERROR, java.util.logging.Level.SEVERE);
            put(Level.WARN, java.util.logging.Level.WARNING);
            put(Level.INFO, java.util.logging.Level.INFO);
            put(Level.DEBUG, java.util.logging.Level.FINE);
            put(Level.TRACE, java.util.logging.Level.FINE);
            put(Level.ALL, java.util.logging.Level.ALL);
        }
    };

    @Override
    public void publish(LogRecord record) {
        // normalize levels
        Logger log4jLogger = Logger.getLogger(record.getLoggerName());
        Level log4jLevel = log4jLogger.getEffectiveLevel();
        java.util.logging.Logger jdkLogger = java.util.logging.Logger.getLogger(record.getLoggerName());
        java.util.logging.Level expectedJdkLevel = LEVELS_LOG4J_TO_JDK.get(log4jLevel);
        if (expectedJdkLevel == null)
            throw new AssertionError("Level not supported yet - have a bug !" + log4jLevel);
        if (!expectedJdkLevel.equals(jdkLogger.getLevel())) {
            jdkLogger.setLevel(expectedJdkLevel);
        }
        log4jLogger.log(record.getLoggerName(), LEVELS_JDK_TO_LOG4J.get(record.getLevel()), record.getMessage(), record.getThrown());
    }

    @Override
    public void flush() {
    }

    @Override
    public void close() throws SecurityException {
    }

    public static void install() {
        LogManager.getLogManager().reset();
        LogManager.getLogManager().getLogger("").addHandler(new JdkOverLog4j());
        LogManager.getLogManager().getLogger("").setLevel(java.util.logging.Level.ALL);
    }
}

Simply put this class in your webapp and at the startup of your webapp, run the code:

JdkOverLog4j.install();

Now all calls to the JDK Logging system are redirected to Log4j !!!

So now you have the best of both world:
  • You are using the standard java.util.logging API
  • You are not binding your code to any logging library or wrapper
  • The client can decide which system he will use
  • You can still use log4j at the very end, but your application and libraries will be even more loosely coupled to it