Search code examples
javaconfigurationjava.util.logging

How do I configure Java Util Logging to show logging as log4j?


I've tried several options (I really tried) but I cannot get a formatting similar to Log4j. In short, I would like to output log lines with the following format:

2014-09-24 14:05:18 INFO  CustomerController:96 - Starting purchase order...
------------------- ----- ---------------------   --------------------------
Timestamp           Level Class & line            Message

In log4j I used to set the pattern:

d{YYYY-MM-dd HH:mm:ss} %-5p %C{1}(%L) - %m%n

That is:

  • A single log line.
  • A local timestamp (no time zone)
  • A 5-letter level. "INFO" is padded with a single space.
  • Class name (no package) and line number.
  • Message.

I'm OK to use an internal config file in the deployable JAR, or using internal Java programmatic configuration. As long as I don't need to deploy an external "extra" file that would work for our environment.


Solution

  • Log messages are formatted by a java.util.logging.Formatter. Unfortunately, neither of the built-in implementations seem to provide what you want. The closest you can get is a SimpleFormatter with a format of %1$TF %1$TT %4$-5s - %5$s%6$s (the %6$s is for exceptions). That will give you everything you want except the class name and line number. You can modify the format to include the source, but there doesn't seem to be a way to omit the package (or method name). And there's no way to define a format that includes the line number.

    I think your only option is to create your own Formatter implementation (or find an existing third-party implementation). The following seems to meet your requirements:

    package com.example;
    
    import java.io.PrintWriter;
    import java.io.StringWriter;
    import java.lang.StackWalker.StackFrame;
    import java.time.ZoneId;
    import java.time.format.DateTimeFormatter;
    import java.util.function.Predicate;
    import java.util.function.Supplier;
    import java.util.logging.Formatter;
    import java.util.logging.LogRecord;
    import java.util.logging.Logger;
    
    public class CustomFormatter extends Formatter {
    
      // formatter used to format the log's timestamp
      private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("uuuu-MM-dd hh:mm:ss");
    
      @Override
      public String format(LogRecord record) {
        var builder = new StringBuilder();
        appendDateTime(record, builder);
        appendLevel(record, builder);
        appendClassNameAndLineNumber(record, builder);
        appendMessage(record, builder);
        appendThrown(record, builder);
        return builder.toString();
      }
    
      private void appendDateTime(LogRecord record, StringBuilder builder) {
        var zdt = record.getInstant().atZone(ZoneId.systemDefault());
        builder.append(DTF.format(zdt));
      }
    
      private void appendLevel(LogRecord record, StringBuilder builder) {
        var name = record.getLevel().getName();
        builder.append(" ");
    
        // You seem to want the name to be padded *or* truncated in order to
        // fit within 5 spaces. Note this logic won't necessarily work if you
        // change the call from 'getName()' to 'getLocalizedName()'. Also, if
        // a custom level's name contains surrogate pairs, then they could
        // possibly be "cut in half".
        switch (name) {
          case "FINEST"  -> builder.append("FINST");
          case "FINER"   -> builder.append("FINER");
          case "FINE"    -> builder.append("FINE ");
          case "CONFIG"  -> builder.append("CNFIG");
          case "INFO"    -> builder.append("INFO ");
          case "WARNING" -> builder.append("WARN ");
          case "SEVERE"  -> builder.append("SEVRE");
          default -> {
            int len = name.length();
            if (len == 5) {
              builder.append(name);
            } else if (len < 5) {
              // StringBuilder::repeat added in Java 21
              builder.append(name).repeat(" ", 5 - len);
            } else {
              builder.append(name, 0, 5);
            }
          }
        }
      }
    
      private void appendClassNameAndLineNumber(LogRecord record, StringBuilder builder) {
        var frame = new SourceFinder().get();
        if (frame != null) {
          var className = frame.getClassName();
          int index = className.lastIndexOf('.');
          if (index != -1) {
            className = className.substring(index + 1);
          }
          builder.append(" ").append(className).append(":").append(frame.getLineNumber());
        }
      }
    
      private void appendMessage(LogRecord record, StringBuilder builder) {
        builder.append(" - ").append(formatMessage(record)).append(System.lineSeparator());
      }
    
      private void appendThrown(LogRecord record, StringBuilder builder) {
        var thrown = record.getThrown();
        if (thrown != null) {
          var sw = new StringWriter();
          thrown.printStackTrace(new PrintWriter(sw, true));
          builder.append(sw.toString());
        }
      }
    
      // Inspired by the private java.util.logging.LogRecord$CallerFinder class
      private static class SourceFinder implements Predicate<StackFrame>, Supplier<StackFrame> {
    
        private static final StackWalker WALKER = StackWalker.getInstance();
    
        private boolean foundLogger;
    
        @Override
        public StackFrame get() {
          foundLogger = false;
          return WALKER.walk(s -> s.filter(this).findFirst()).orElse(null);
        }
    
        @Override
        public boolean test(StackFrame t) {
          if (!foundLogger) {
            foundLogger = t.getClassName().equals(Logger.class.getName());
            return false;
          }
          return !t.getClassName().startsWith("java.util.logging");
        }
      }
    }
    

    You may want to increase the "padding" used by the level name. Of just the predefined levels, there's WARNING which has a length of 7. And that's ignoring whether or not you want to modify the above to get the level's localized name instead.

    Here's an example using the above formatter:

    package com.example;
    
    import java.util.logging.Logger;
    
    public class Main {
    
      static {
        var root = Logger.getLogger("");
        for (var handler : root.getHandlers()) {
          handler.setFormatter(new CustomFormatter());
        }
      }
    
      public static void main(String[] args) throws Exception {
        var logger = Logger.getLogger(Main.class.getName());
        logger.info("This is a test message..."); // line 16
      }
    }
    

    Output:

    2024-09-24 09:42:45 INFO  Main:16 - This is a test message...