From: Mathieu Baudier Date: Fri, 31 Dec 2021 07:07:29 +0000 (+0100) Subject: Make logging configurable X-Git-Tag: argeo-commons-2.3.5~114 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=8be06a167888a8ba1f262bbe34c70385807d11c0;p=lgpl%2Fargeo-commons.git Make logging configurable --- 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 index e6f4c62ca..000000000 --- a/org.argeo.init/src/org/argeo/init/logging/ThinHandler.java +++ /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 diff --git a/org.argeo.init/src/org/argeo/init/logging/ThinJavaUtilLogging.java b/org.argeo.init/src/org/argeo/init/logging/ThinJavaUtilLogging.java index 7670c2573..f1143d670 100644 --- a/org.argeo.init/src/org/argeo/init/logging/ThinJavaUtilLogging.java +++ b/org.argeo.init/src/org/argeo/init/logging/ThinJavaUtilLogging.java @@ -1,18 +1,106 @@ 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 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 { + } + } } diff --git a/org.argeo.init/src/org/argeo/init/logging/ThinLoggerFinder.java b/org.argeo.init/src/org/argeo/init/logging/ThinLoggerFinder.java index 9607c9478..4147534dd 100644 --- a/org.argeo.init/src/org/argeo/init/logging/ThinLoggerFinder.java +++ b/org.argeo.init/src/org/argeo/init/logging/ThinLoggerFinder.java @@ -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 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 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); } } diff --git a/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java b/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java index 2c2b04d2c..22c1c779b 100644 --- a/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java +++ b/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java @@ -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 loggers = new TreeMap<>(); private NavigableMap 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 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 configuration) { + synchronized (levels) { + updatingConfiguration = true; + + Map 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> it = levels.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry 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 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":