X-Git-Url: https://git.argeo.org/?a=blobdiff_plain;f=org.argeo.init%2Fsrc%2Forg%2Fargeo%2Finit%2Flogging%2FThinLogging.java;h=e03b179a35bc947f8612266555e34a62a6c7bc83;hb=b6ee9c8baa2650ebbca59c9f24eac3599408bf7f;hp=871dc8d7f72d2b6ee401e544458b7f1455e6f803;hpb=0ffddd13968b41b26caddfcf17b3dc86a58eb776;p=lgpl%2Fargeo-commons.git 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 871dc8d7f..e03b179a3 100644 --- a/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java +++ b/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java @@ -16,16 +16,17 @@ import java.util.ResourceBundle; import java.util.SortedMap; import java.util.StringTokenizer; import java.util.TreeMap; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.Flow; import java.util.concurrent.Flow.Subscription; +import java.util.concurrent.ForkJoinPool; import java.util.concurrent.SubmissionPublisher; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; +import org.argeo.init.RuntimeContext; +import org.argeo.init.Service; + /** * A thin logging system based on the {@link Logger} framework. It is a * {@link Consumer} of configuration, and can be registered as such. @@ -36,8 +37,12 @@ class ThinLogging implements Consumer> { final static String DEFAULT_LEVEL_PROPERTY = "log"; final static String LEVEL_PROPERTY_PREFIX = DEFAULT_LEVEL_PROPERTY + "."; - final static String JOURNALD_PROPERTY = "argeo.logging.journald"; - final static String CALL_LOCATION_PROPERTY = "argeo.logging.callLocation"; + final static String PROP_ARGEO_LOGGING_SYNCHRONOUS = "argeo.logging.synchronous"; + final static String PROP_ARGEO_LOGGING_JOURNALD = "argeo.logging.journald"; + final static String PROP_ARGEO_LOGGING_CALL_LOCATION = "argeo.logging.callLocation"; + + final static String ENV_INVOCATION_ID = "INVOCATION_ID"; + final static String ENV_GIO_LAUNCHED_DESKTOP_FILE_PID = "GIO_LAUNCHED_DESKTOP_FILE_PID"; private final static AtomicLong nextEntry = new AtomicLong(0l); @@ -48,27 +53,36 @@ class ThinLogging implements Consumer> { private NavigableMap levels = new TreeMap<>(); private volatile boolean updatingConfiguration = false; - private final ExecutorService executor; +// private final ExecutorService executor; private final LogEntryPublisher publisher; + private PrintStreamSubscriber synchronousSubscriber; private final boolean journald; private final Level callLocationLevel; - ThinLogging() { - executor = Executors.newCachedThreadPool((r) -> { - Thread t = new Thread(r); - t.setDaemon(true); - return t; - }); - publisher = new LogEntryPublisher(executor, Flow.defaultBufferSize()); + private boolean synchronous = Boolean.parseBoolean(System.getProperty(PROP_ARGEO_LOGGING_SYNCHRONOUS)); - PrintStreamSubscriber subscriber = new PrintStreamSubscriber(); - publisher.subscribe(subscriber); + ThinLogging() { +// executor = Executors.newCachedThreadPool((r) -> { +// Thread t = new Thread(r); +// t.setDaemon(true); +// return t; +// }); + if (synchronous) { + publisher = new LogEntryPublisher(); + synchronousSubscriber = new PrintStreamSubscriber(); + } else { + publisher = new LogEntryPublisher(); + + PrintStreamSubscriber subscriber = new PrintStreamSubscriber(); + publisher.subscribe(subscriber); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> close(), "Log shutdown")); - Runtime.getRuntime().addShutdownHook(new Thread(() -> close(), "Log shutdown")); + } // initial default level - levels.put("", Level.WARNING); + levels.put(DEFAULT_LEVEL_NAME, Level.WARNING); // Logging system config // journald @@ -78,13 +92,13 @@ class ThinLogging implements Consumer> { // System.out.println(key + "=" + env.get(key)); // } - String journaldStr = System.getProperty(JOURNALD_PROPERTY, "auto"); + String journaldStr = System.getProperty(PROP_ARGEO_LOGGING_JOURNALD, "auto"); switch (journaldStr) { case "auto": - String systemdInvocationId = System.getenv("INVOCATION_ID"); + String systemdInvocationId = System.getenv(ENV_INVOCATION_ID); if (systemdInvocationId != null) {// in systemd - // check whether we are indirectly in a desktop app (e.g. eclipse) - String desktopFilePid = System.getenv("GIO_LAUNCHED_DESKTOP_FILE_PID"); + // check whether we are indirectly in a desktop app (typically an IDE) + String desktopFilePid = System.getenv(ENV_GIO_LAUNCHED_DESKTOP_FILE_PID); if (desktopFilePid != null) { Long javaPid = ProcessHandle.current().pid(); if (!javaPid.toString().equals(desktopFilePid)) { @@ -107,19 +121,29 @@ class ThinLogging implements Consumer> { break; default: throw new IllegalArgumentException( - "Unsupported value '" + journaldStr + "' for property " + JOURNALD_PROPERTY); + "Unsupported value '" + journaldStr + "' for property " + PROP_ARGEO_LOGGING_JOURNALD); } - String callLocationStr = System.getProperty(CALL_LOCATION_PROPERTY, Level.WARNING.getName()); + String callLocationStr = System.getProperty(PROP_ARGEO_LOGGING_CALL_LOCATION, Level.WARNING.getName()); callLocationLevel = Level.valueOf(callLocationStr); } private void close() { + RuntimeContext runtimeContext = Service.getRuntimeContext(); + if (runtimeContext != null) { + try { + runtimeContext.waitForStop(0); + } catch (InterruptedException e) { + // silent + } + } + publisher.close(); try { - // we ait a bit in order to make sure all messages are flushed + // we wait a bit in order to make sure all messages are flushed // TODO synchronize more efficiently - executor.awaitTermination(300, TimeUnit.MILLISECONDS); + // executor.awaitTermination(300, TimeUnit.MILLISECONDS); + ForkJoinPool.commonPool().awaitTermination(300, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { // silent } @@ -128,28 +152,13 @@ class ThinLogging implements Consumer> { private Level computeApplicableLevel(String name) { Map.Entry entry = levels.floorEntry(name); assert entry != null; - return entry.getValue(); + if (name.startsWith(entry.getKey())) + return entry.getValue(); + else + return levels.get(DEFAULT_LEVEL_NAME);// default } -// private 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); -// } -// } -// } -// -// return level.getSeverity() >= computeApplicableLevel(name).getSeverity(); -// } - public Logger getLogger(String name, Module module) { if (!loggers.containsKey(name)) { ThinLogger logger = new ThinLogger(name, computeApplicableLevel(name)); @@ -202,7 +211,6 @@ class ThinLogging implements Consumer> { updatingConfiguration = false; levels.notifyAll(); } - } Flow.Publisher> getLogEntryPublisher() { @@ -274,6 +282,19 @@ class ThinLogging implements Consumer> { // 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 +// if (format.contains("{}"))// workaround for weird Jetty formatting +// params = null; + // TODO move this to slf4j wrapper? + if (format.contains("{}")) { + StringBuilder sb = new StringBuilder(); + String[] segments = format.split("\\{\\}"); + for (int i = 0; i < segments.length; i++) { + sb.append(segments[i]); + if (i != (segments.length - 1)) + sb.append("{" + i + "}"); + } + format = sb.toString(); + } String msg = params == null ? format : MessageFormat.format(format, params); publisher.log(this, level, bundle, msg, now, thread, (Throwable) null, findCallLocation(level, thread)); } @@ -285,6 +306,7 @@ class ThinLogging implements Consumer> { private StackTraceElement findCallLocation(Level level, Thread thread) { assert level != null; assert thread != null; + // TODO rather use a StackWalker and make it smarter StackTraceElement callLocation = null; if (level.getSeverity() >= callLocationLevel.getSeverity()) { StackTraceElement[] stack = thread.getStackTrace(); @@ -293,12 +315,16 @@ class ThinLogging implements Consumer> { String className = stack[i].getClassName(); switch (className) { // TODO make it more configurable + // FIXME deal with privileges stacks (in Equinox) case "java.lang.System$Logger": case "java.util.logging.Logger": case "org.apache.commons.logging.Log": case "org.osgi.service.log.Logger": + case "org.eclipse.osgi.internal.log.LoggerImpl": case "org.argeo.api.cms.CmsLog": + case "org.argeo.init.osgi.OsgiBootUtils": case "org.slf4j.impl.ArgeoLogger": + case "org.argeo.cms.internal.osgi.CmsOsgiLogger": case "org.eclipse.jetty.util.log.Slf4jLog": case "sun.util.logging.internal.LoggingProviderImpl$JULWrapper": lowestLoggerInterface = i; @@ -324,8 +350,8 @@ class ThinLogging implements Consumer> { private class LogEntryPublisher extends SubmissionPublisher> { - private LogEntryPublisher(Executor executor, int maxBufferCapacity) { - super(executor, maxBufferCapacity); + private LogEntryPublisher() { + super(); } private void log(ThinLogger logger, Level level, ResourceBundle bundle, String msg, Instant instant, @@ -354,7 +380,13 @@ class ThinLogging implements Consumer> { logEntry.put(KEY_THREAD, thread.getName()); // should be unmodifiable for security reasons - submit(Collections.unmodifiableMap(logEntry)); + if (synchronous) { + assert synchronousSubscriber != null; + synchronousSubscriber.onNext(logEntry); + } else { + if (!isClosed()) + submit(Collections.unmodifiableMap(logEntry)); + } } } @@ -401,6 +433,8 @@ class ThinLogging implements Consumer> { private PrintStream err; private int writeToErrLevel = Level.WARNING.getSeverity(); + private Subscription subscription; + protected PrintStreamSubscriber() { this(System.out, System.err); } @@ -416,7 +450,8 @@ class ThinLogging implements Consumer> { @Override public void onSubscribe(Subscription subscription) { - subscription.request(Long.MAX_VALUE); + this.subscription = subscription; + this.subscription.request(1); } @Override @@ -427,6 +462,7 @@ class ThinLogging implements Consumer> { out.print(toPrint(item)); } // TODO flush for journald? + this.subscription.request(1); } @Override @@ -488,10 +524,9 @@ class ThinLogging implements Consumer> { protected String toPrint(Map logEntry) { StringBuilder sb = new StringBuilder(); StringTokenizer st = new StringTokenizer((String) logEntry.get(KEY_MSG), "\r\n"); - assert st.hasMoreTokens(); // first line - String firstLine = st.nextToken(); + String firstLine = st.hasMoreTokens() ? st.nextToken() : ""; sb.append(firstLinePrefix(logEntry)); sb.append(firstLine); sb.append(firstLineSuffix(logEntry)); @@ -540,6 +575,16 @@ class ThinLogging implements Consumer> { logger.log(Logger.Level.INFO, "Log info"); logger.log(Logger.Level.WARNING, "Log warning"); logger.log(Logger.Level.ERROR, "Log exception", new Throwable()); + + try { + // we ait a bit in order to make sure all messages are flushed + // TODO synchronize more efficiently + // executor.awaitTermination(300, TimeUnit.MILLISECONDS); + ForkJoinPool.commonPool().awaitTermination(300, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // silent + } + } }