load java logging properties from the classpath

When using plain Java logging the configuration file normally must be set by setting the System property java.util.logging.config.file. There also is the possibility to configure the logging system with a configuration class, but this article deals with file configuration.

Using the system property is quite uncomfortable as it involves changing run configurations in the IDE or typing more on the commandline when starting the program. Especially in maven powered builds, where I might wish to have different configurations for test and normal execution this is not an optimal solution.

So I was looking for a way to configure the logging system by using a logging.properties file which is searched in the classpath. The logging properties file I am going to use in this post is:

#  
.level = WARNING  
handlers = java.util.logging.ConsoleHandler com.sothawo.level = ALL

java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter  
java.util.logging.ConsoleHandler.level = ALL

# Timestamp Level Source log message backtrace  
java.util.logging.SimpleFormatter.format = %1$tF %1$tT:%1$tL %1$tZ %4$s %2$s: %5$s %6$s%n  

I want to log everything from my com.sothawo classes, but all other messages should be restricted to level WARNING. The first solution I came up with was:

// SAMPLE CODE! NOT WORKING AS EXPECTED!
package com.sothawo.mapjfx.demo;
public class DemoApp extends Application {
   private static final Logger logger = Logger.getLogger(DemoApp.class.getCanonicalName());

  public static void main(String[] args) {
    InputStream inputStream = DemoApp.class.getResourceAsStream("/logging.properties");
    if (null != inputStream) {
      try {
        LogManager.getLogManager().readConfiguration(inputStream);
      } catch (IOException e) {
        Logger.getGlobal().log(Level.SEVERE, "init logging system", e);
      }
    }
    logger.fine("fine test logging");
    }
  }
}

This did not work. Debugging through the code I found that the input stream was loaded and the LogManager was reconfigured, but my logger was still configured to INFO level, which is the default and therefore the fine message was not logged. I checked the Javadoc for LogManager.readConfiguration() and there it says: “Any log level definitions in the new configuration file will be applied using Logger.setLevel(), if the target Logger exists.“. So why was my logger not set to the correct level?

I dug into the code for LogManager and found that only the level of the loggers with names that are exactly the same as the names defined in the properties are set and not the levels of loggers which have a configured logger in their parent chain. In my case: In the properties I have the line
com.sothawo.level = ALL. This sets the level for a logger named com.sothawo but in my code the logger has the name com.sothawo.mapjfx.demo.DemoApp, and so it is not set. The LogManager does not consider the parental relationships when reading the configuration, this is only done when a logger is created.

So care has to be taken and the logger must be created after reading the configuration:

// WORKING CODE BUT UGLY
package com.sothawo.mapjfx.demo;
public class DemoApp extends Application {

  private static Logger logger; // NOT FINAL ANY MORE!

  public static void main(String[] args) {
    InputStream inputStream = DemoApp.class.getResourceAsStream("/logging.properties");
    if (null != inputStream) {
      try {
        LogManager.getLogManager().readConfiguration(inputStream);
      } catch (IOException e) {
        Logger.getGlobal().log(Level.SEVERE, "init logging system", e);
      }
    }
    logger = Logger.getLogger(DemoApp.class.getCanonicalName());
    logger.fine("fine test logging");
    }
  }
}

This I consider to be ugly code, as I like my loggers to be final, and it clutters up the main method.

So my final solution is to put the relevant code in a static initializer:

package com.sothawo.mapjfx.demo;
public class DemoApp extends Application {
  private static final Logger logger;

  static {
    InputStream inputStream = DemoApp.class.getResourceAsStream("/logging.properties");
    if (null != inputStream) {
    try {
      LogManager.getLogManager().readConfiguration(inputStream);
    } catch (IOException e) {
      Logger.getGlobal().log(Level.SEVERE, "init logging system", e);
    }
    logger = Logger.getLogger(DemoApp.class.getCanonicalName());
  }

  public static void main(String[] args) {
    logger.fine("fine test logging");
  }
}