Make logging configurable
authorMathieu Baudier <mbaudier@argeo.org>
Fri, 31 Dec 2021 07:07:29 +0000 (08:07 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Fri, 31 Dec 2021 07:07:29 +0000 (08:07 +0100)
org.argeo.init/src/org/argeo/init/logging/ThinHandler.java [deleted file]
org.argeo.init/src/org/argeo/init/logging/ThinJavaUtilLogging.java
org.argeo.init/src/org/argeo/init/logging/ThinLoggerFinder.java
org.argeo.init/src/org/argeo/init/logging/ThinLogging.java

diff --git a/org.argeo.init/src/org/argeo/init/logging/ThinHandler.java b/org.argeo.init/src/org/argeo/init/logging/ThinHandler.java
deleted file mode 100644 (file)
index e6f4c62..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-package org.argeo.init.logging;
-
-import java.lang.System.Logger.Level;
-import java.util.logging.Handler;
-import java.util.logging.LogRecord;
-
-/**
- * A fallback {@link Handler} forwarding only messages and logger name (all
- * other {@link LogRecord} information is lost.
- */
-class ThinHandler extends Handler {
-       @Override
-       public void publish(LogRecord record) {
-               java.lang.System.Logger systemLogger = ThinLoggerFinder.getLogger(record.getLoggerName());
-               systemLogger.log(fromJulLevel(record.getLevel()), record.getMessage());
-       }
-
-       protected Level fromJulLevel(java.util.logging.Level julLevel) {
-               if (java.util.logging.Level.ALL.equals(julLevel))
-                       return Level.ALL;
-               else if (java.util.logging.Level.FINER.equals(julLevel))
-                       return Level.TRACE;
-               else if (java.util.logging.Level.FINE.equals(julLevel))
-                       return Level.DEBUG;
-               else if (java.util.logging.Level.INFO.equals(julLevel))
-                       return Level.INFO;
-               else if (java.util.logging.Level.WARNING.equals(julLevel))
-                       return Level.WARNING;
-               else if (java.util.logging.Level.SEVERE.equals(julLevel))
-                       return Level.ERROR;
-               else if (java.util.logging.Level.OFF.equals(julLevel))
-                       return Level.OFF;
-               else
-                       throw new IllegalArgumentException("Unsupported JUL level " + julLevel);
-       }
-
-       @Override
-       public void flush() {
-       }
-
-       @Override
-       public void close() throws SecurityException {
-       }
-
-}
\ No newline at end of file
index 7670c2573a2c9deba0f1085b0c2d6377f56211d0..f1143d670af8647746702bdd361cd721c7647b57 100644 (file)
 package org.argeo.init.logging;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.lang.System.Logger.Level;
+import java.util.Map;
+import java.util.Properties;
+import java.util.logging.Handler;
 import java.util.logging.LogManager;
-import java.util.logging.Logger;
+import java.util.logging.LogRecord;
 
 /**
- * Fallback wrapper around the java.util.logging framework, when thinb logging
+ * Fallback wrapper around the java.util.logging framework, when thin logging
  * could not be instantiated directly.
  */
 class ThinJavaUtilLogging {
-       public static void init() {
+       private LogManager logManager;
+
+       private ThinJavaUtilLogging(LogManager logManager) {
+               this.logManager = logManager;
+               this.logManager.reset();
+       }
+
+       static ThinJavaUtilLogging init() {
                LogManager logManager = LogManager.getLogManager();
-               logManager.reset();
-               Logger rootLogger = logManager.getLogger("");
-               rootLogger.addHandler(new ThinHandler());
-               rootLogger.setLevel(java.util.logging.Level.INFO);
+               ThinJavaUtilLogging thinJul = new ThinJavaUtilLogging(logManager);
+               return thinJul;
+       }
+
+       private static Level fromJulLevel(java.util.logging.Level julLevel) {
+               if (java.util.logging.Level.ALL.equals(julLevel))
+                       return Level.ALL;
+               else if (java.util.logging.Level.FINER.equals(julLevel))
+                       return Level.TRACE;
+               else if (java.util.logging.Level.FINE.equals(julLevel))
+                       return Level.DEBUG;
+               else if (java.util.logging.Level.INFO.equals(julLevel))
+                       return Level.INFO;
+               else if (java.util.logging.Level.WARNING.equals(julLevel))
+                       return Level.WARNING;
+               else if (java.util.logging.Level.SEVERE.equals(julLevel))
+                       return Level.ERROR;
+               else if (java.util.logging.Level.OFF.equals(julLevel))
+                       return Level.OFF;
+               else
+                       throw new IllegalArgumentException("Unsupported JUL level " + julLevel);
+       }
+
+       private static java.util.logging.Level toJulLevel(Level level) {
+               if (Level.ALL.equals(level))
+                       return java.util.logging.Level.ALL;
+               else if (Level.TRACE.equals(level))
+                       return java.util.logging.Level.FINER;
+               else if (Level.DEBUG.equals(level))
+                       return java.util.logging.Level.FINE;
+               else if (Level.INFO.equals(level))
+                       return java.util.logging.Level.INFO;
+               else if (Level.WARNING.equals(level))
+                       return java.util.logging.Level.WARNING;
+               else if (Level.ERROR.equals(level))
+                       return java.util.logging.Level.SEVERE;
+               else if (Level.OFF.equals(level))
+                       return java.util.logging.Level.OFF;
+               else
+                       throw new IllegalArgumentException("Unsupported logging level " + level);
+       }
+
+       void readConfiguration(Map<String, Level> configuration) {
+               this.logManager.reset();
+               Properties properties = new Properties();
+               for (String name : configuration.keySet()) {
+                       properties.put(name + ".level", toJulLevel(configuration.get(name)).toString());
+               }
+               try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+                       properties.store(out, null);
+                       try (ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray())) {
+                               logManager.readConfiguration(in);
+                       }
+               } catch (IOException e) {
+                       throw new IllegalStateException("Cannot apply JUL configuration", e);
+               }
+               logManager.getLogger("").addHandler(new ThinHandler());
+       }
+
+       /**
+        * A fallback {@link Handler} forwarding only messages and logger name (all
+        * other {@link LogRecord} information is lost.
+        */
+       private static class ThinHandler extends Handler {
+               @Override
+               public void publish(LogRecord record) {
+                       java.lang.System.Logger systemLogger = ThinLoggerFinder.getLogger(record.getLoggerName());
+                       systemLogger.log(ThinJavaUtilLogging.fromJulLevel(record.getLevel()), record.getMessage());
+               }
+
+               @Override
+               public void flush() {
+               }
+
+               @Override
+               public void close() throws SecurityException {
+               }
+
        }
 }
index 9607c9478df78c3f5f259111a4ed7aab46ef4cee..4147534dd2695abcdc393f7c988ef49965ddc4df 100644 (file)
@@ -2,15 +2,23 @@ package org.argeo.init.logging;
 
 import java.lang.System.Logger;
 import java.lang.System.LoggerFinder;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
 
-/** Factory for Java system logging. */
+/**
+ * Factory for Java system logging. As it has to be a public class in order to
+ * be exposed as a service provider, it is also the main entry point for the
+ * thin logging system, via static methos.
+ */
 public class ThinLoggerFinder extends LoggerFinder {
        private static ThinLogging logging;
+       private static ThinJavaUtilLogging javaUtilLogging;
 
        public ThinLoggerFinder() {
                if (logging != null)
                        throw new IllegalStateException("Only one logging can be initialised.");
-               logging = new ThinLogging();
+               init();
        }
 
        @Override
@@ -18,17 +26,42 @@ public class ThinLoggerFinder extends LoggerFinder {
                return logging.getLogger(name, module);
        }
 
+       private static void init() {
+               logging = new ThinLogging();
+
+               Map<String, String> configuration = new HashMap<>();
+               for (Object key : System.getProperties().keySet()) {
+                       Objects.requireNonNull(key);
+                       String property = key.toString();
+                       if (property.startsWith(ThinLogging.LEVEL_PROPERTY_PREFIX)
+                                       || property.equals(ThinLogging.DEFAULT_LEVEL_PROPERTY))
+                               configuration.put(property, System.getProperty(property));
+               }
+               logging.update(configuration);
+       }
+
        /**
         * Falls back to java.util.logging if thin logging was not already initialised.
         */
        public static void lazyInit() {
                if (logging != null)
                        return;
-               logging = new ThinLogging();
-               ThinJavaUtilLogging.init();
+               if (javaUtilLogging != null)
+                       return;
+               init();
+               javaUtilLogging = ThinJavaUtilLogging.init();
+               javaUtilLogging.readConfiguration(logging.getLevels());
+       }
+
+       public static void update(Map<String, String> configuration) {
+               if (logging == null)
+                       throw new IllegalStateException("Thin logging must be initialized first");
+               logging.update(configuration);
+               if (javaUtilLogging != null)
+                       javaUtilLogging.readConfiguration(logging.getLevels());
        }
 
-       public static Logger getLogger(String name) {
+       static Logger getLogger(String name) {
                return logging.getLogger(name, null);
        }
 }
index 2c2b04d2c7952e50006e3b05c39cb0bca8bf7abf..22c1c779b40363bd9ede703ad641d2839d791c2d 100644 (file)
@@ -5,6 +5,8 @@ import java.lang.System.Logger;
 import java.lang.System.Logger.Level;
 import java.text.MessageFormat;
 import java.time.Instant;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.NavigableMap;
 import java.util.Objects;
@@ -22,8 +24,17 @@ import java.util.concurrent.TimeUnit;
 
 /** A thin logging system based on the {@link Logger} framework. */
 class ThinLogging {
+       final static String DEFAULT_LEVEL_NAME = "";
+
+       final static String DEFAULT_LEVEL_PROPERTY = "log";
+       final static String LEVEL_PROPERTY_PREFIX = DEFAULT_LEVEL_PROPERTY + ".";
+
+       // we don't synchronize maps on purpose as it would be
+       // too expensive during normal operation
+       // updates to the config may be shortly inconsistent
        private SortedMap<String, ThinLogger> loggers = new TreeMap<>();
        private NavigableMap<String, Level> levels = new TreeMap<>();
+       private volatile boolean updatingConfiguration = false;
 
        private final ExecutorService executor;
        private final LogEntryPublisher publisher;
@@ -41,7 +52,8 @@ class ThinLogging {
 
                Runtime.getRuntime().addShutdownHook(new Thread(() -> close(), "Log shutdown"));
 
-               setDefaultLevel(Level.INFO);
+               // initial default level
+               levels.put("", Level.WARNING);
        }
 
        protected void close() {
@@ -55,14 +67,21 @@ class ThinLogging {
                }
        }
 
-       public void setDefaultLevel(Level level) {
-               levels.put("", level);
-       }
-
        public boolean isLoggable(String name, Level level) {
                Objects.requireNonNull(name);
                Objects.requireNonNull(level);
 
+               if (updatingConfiguration) {
+                       synchronized (levels) {
+                               try {
+                                       levels.wait();
+                                       // TODO make exit more robust
+                               } catch (InterruptedException e) {
+                                       throw new IllegalStateException(e);
+                               }
+                       }
+               }
+
                Map.Entry<String, Level> entry = levels.ceilingEntry(name);
                assert entry != null;
                return level.getSeverity() >= entry.getValue().getSeverity();
@@ -76,6 +95,56 @@ class ThinLogging {
                return loggers.get(name);
        }
 
+       void update(Map<String, String> configuration) {
+               synchronized (levels) {
+                       updatingConfiguration = true;
+
+                       Map<String, Level> backup = new TreeMap<>(levels);
+
+                       boolean fullReset = configuration.containsKey(DEFAULT_LEVEL_PROPERTY);
+                       try {
+                               properties: for (String property : configuration.keySet()) {
+                                       if (!property.startsWith(LEVEL_PROPERTY_PREFIX))
+                                               continue properties;
+                                       String levelStr = configuration.get(property);
+                                       Level level = Level.valueOf(levelStr);
+                                       levels.put(property.substring(LEVEL_PROPERTY_PREFIX.length()), level);
+                               }
+
+                               if (fullReset) {
+                                       Iterator<Map.Entry<String, Level>> it = levels.entrySet().iterator();
+                                       while (it.hasNext()) {
+                                               Map.Entry<String, Level> entry = it.next();
+                                               String name = entry.getKey();
+                                               if (!configuration.containsKey(LEVEL_PROPERTY_PREFIX + name)) {
+                                                       it.remove();
+                                               }
+                                       }
+//                             for (String name : levels.keySet()) {
+//                                     if (!configuration.containsKey(LEVEL_PROPERTY_PREFIX + name)) {
+//                                             levels.remove(name);
+//                                     }
+//                             }
+                                       Level newDefaultLevel = Level.valueOf(configuration.get(DEFAULT_LEVEL_PROPERTY));
+                                       levels.put(DEFAULT_LEVEL_NAME, newDefaultLevel);
+                                       // TODO notify everyone?
+                               }
+                               assert levels.containsKey(DEFAULT_LEVEL_NAME);
+                       } catch (IllegalArgumentException e) {
+                               e.printStackTrace();
+                               levels.clear();
+                               levels.putAll(backup);
+                       }
+                       updatingConfiguration = false;
+                       levels.notifyAll();
+               }
+
+       }
+
+       Map<String, Level> getLevels() {
+               return Collections.unmodifiableNavigableMap(levels);
+       }
+
        class ThinLogger implements System.Logger {
                private final String name;
                private boolean callLocationEnabled = true;
@@ -92,6 +161,7 @@ class ThinLogging {
 
                @Override
                public boolean isLoggable(Level level) {
+                       // TODO optimise by referencing the applicable level in this class?
                        return ThinLogging.this.isLoggable(name, level);
                }
 
@@ -106,6 +176,9 @@ class ThinLogging {
                public void log(Level level, ResourceBundle bundle, String format, Object... params) {
                        // measure timestamp first
                        Instant now = Instant.now();
+
+                       // NOTE: this is the method called when logging a plain message without
+                       // exception, so it should be considered as a format only when args are not null
                        String msg = params == null ? format : MessageFormat.format(format, params);
                        publisher.log(this, level, bundle, msg, null, now, findCallLocation());
                }
@@ -113,14 +186,12 @@ class ThinLogging {
                protected StackTraceElement findCallLocation() {
                        StackTraceElement callLocation = null;
                        if (callLocationEnabled) {
-//                             Throwable locator = new Throwable();
-//                             StackTraceElement[] stack = locator.getStackTrace();
                                StackTraceElement[] stack = Thread.currentThread().getStackTrace();
-                               // TODO make it smarter by finding the lowest logger interface in the stack
                                int lowestLoggerInterface = 0;
                                stack: for (int i = 2; i < stack.length; i++) {
                                        String className = stack[i].getClassName();
                                        switch (className) {
+                                       // TODO make it more configurable
                                        case "java.lang.System$Logger":
                                        case "java.util.logging.Logger":
                                        case "org.apache.commons.logging.Log":