2013/03/27

Renamed Logback Config File for Web Application

There are four frameworks frequently used for logging in Java - java.util.logging aka JUL, Apache Commons Logging aka JCL, log4j 1.x, and slf4j. I had "luck" to find them all used by a single web application.

It was impossible to tell what component uses what logging API for what messages. To make the logging manageable I modified the code base to use SLF4J API with Logback - changes to several places in code were made and bridges were added for third-party libraries.

The last request was to name the configuration file uniquely so it can be in the same directory with configuration files for other applications. The problem is Logback  is not particularly  flexible when it comes to the naming of its config files - it looks for logback.groovy,  for logback-test.xml, logback.xml and then fails with JoranException.

According to documentation, there is a system property logback.configurationFile that allows to set a different name for the file.

It can be set on a command line (java -Dlogback.configurationFile=/path/to/config.xml), but that is not what you want or can use for a web application. Usual way how to pass settings to a web application is to use <context-param> in web.xml:
<context-param>
  <param-name>logback.configurationFile</param-name>
  <param-value>custom-logback.xml</param-value>
</context-param> 

The problem is that servlet context parameters are not copied into system properties, so SLF4J ContextInitializer ends up looking for the "usual suspects"  and then gives up:

ContextInitializer.autoConfig()
     -> findURLOfDefaultConfigurationFile()
         -> findConfigFileURLFromSystemProperties()
            // logback.configurationFile is not a system property, returns null:    
            -> OptionHelper.getSystemProperty(CONFIG_FILE_PROPERTY)
         -> getResource("logback.groovy")
         -> getResource("logback-test.xml")
         -> getResource("logback.xml")
         : returns null 
      -> 3/ BasicConfigurator.configure(loggerContext)



The solution is to get the  context-param as soon as possible during web application initialization and pass it to JoranConfigurator. You have to implement custom ServletContextListener and put the logic inside contextInitialized method:

public class CustomServletContextListener 
implements ServletContextListener 
{
  @Override
  public void contextInitialized(ServletContextEvent contextEvent) 
  { 
    String logbackConfigFile = contextEvent.getServletContext().getInitParameter("logback.configurationFile"); 
    URL configFileURL;
    if (logbackConfigFile != null) 
    { 
      configFileURL = Thread.currentThread().getContextClassLoader().getResource(logbackConfigFile);
      if (configFileURL != null)
      {
        JoranConfigurator configurator = new JoranConfigurator(); 
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); 
        loggerContext.reset(); 
        configurator.setContext(loggerContext); 
        try { configurator.doConfigure(configFileURL); } 
        catch (JoranException ex) { throw new RuntimeException(ex); } 
      }
    }
  } 
To be sure the initialization happens as soon as possible, put the context listener to the top of the listeners list in web.xml:
<listener>
  <listener-class>org.bithill.CustomServletContextListener</listener-class>
</listener>