]> git.argeo.org Git - lgpl/argeo-commons.git/blobdiff - org.argeo.init/src/org/argeo/init/logging/ThinLogging.java
Make logging configurable
[lgpl/argeo-commons.git] / org.argeo.init / src / org / argeo / init / logging / ThinLogging.java
index e7edc19a89e9858b9a7ee0d1ba9eb0e21f97f45c..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,19 +24,22 @@ import java.util.concurrent.TimeUnit;
 
 /** A thin logging system based on the {@link Logger} framework. */
 class ThinLogging {
-//     private static ThinLogging instance;
+       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;
 
        ThinLogging() {
-//             if (instance != null)
-//                     throw new IllegalStateException("Only one logger finder cann be instantiated");
-//             instance = this;
-
                executor = Executors.newCachedThreadPool((r) -> {
                        Thread t = new Thread(r);
                        t.setDaemon(true);
@@ -46,6 +51,9 @@ class ThinLogging {
                publisher.subscribe(subscriber);
 
                Runtime.getRuntime().addShutdownHook(new Thread(() -> close(), "Log shutdown"));
+
+               // initial default level
+               levels.put("", Level.WARNING);
        }
 
        protected void close() {
@@ -59,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();
@@ -80,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;
@@ -96,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);
                }
 
@@ -110,25 +176,29 @@ class ThinLogging {
                public void log(Level level, ResourceBundle bundle, String format, Object... params) {
                        // measure timestamp first
                        Instant now = Instant.now();
-                       String msg = MessageFormat.format(format, params);
+
+                       // 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());
                }
 
                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":
                                        case "org.osgi.service.log.Logger":
                                        case "org.argeo.cms.Log":
+                                       case "org.slf4j.impl.ArgeoLogger":
+                                       case "org.eclipse.jetty.util.log.Slf4jLog":
                                                lowestLoggerInterface = i;
                                                continue stack;
                                        default: