X-Git-Url: http://git.argeo.org/?a=blobdiff_plain;f=org.argeo.init%2Fsrc%2Forg%2Fargeo%2Finit%2Flogging%2FThinLogging.java;h=dd6fad2e3a5a54c261517b02f720cacf9f925278;hb=c7b612ca9bd7df43b0bec37c8abcae846587c978;hp=e03b179a35bc947f8612266555e34a62a6c7bc83;hpb=b6ee9c8baa2650ebbca59c9f24eac3599408bf7f;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 e03b179a3..dd6fad2e3 100644 --- a/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java +++ b/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java @@ -1,9 +1,14 @@ package org.argeo.init.logging; +import java.io.IOException; import java.io.PrintStream; import java.io.Serializable; import java.lang.System.Logger; import java.lang.System.Logger.Level; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.text.MessageFormat; import java.time.Instant; import java.util.Collections; @@ -24,8 +29,8 @@ 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; +import org.argeo.api.init.RuntimeContext; +import org.argeo.internal.init.InternalState; /** * A thin logging system based on the {@link Logger} framework. It is a @@ -37,9 +42,23 @@ class ThinLogging implements Consumer> { final static String DEFAULT_LEVEL_PROPERTY = "log"; final static String LEVEL_PROPERTY_PREFIX = DEFAULT_LEVEL_PROPERTY + "."; + /** + * Whether logged event are only immediately printed to the standard output. + * Required for native images. + */ final static String PROP_ARGEO_LOGGING_SYNCHRONOUS = "argeo.logging.synchronous"; + /** + * Whether to enable journald compatible output, either: auto (default), true, + * or false. + */ final static String PROP_ARGEO_LOGGING_JOURNALD = "argeo.logging.journald"; - final static String PROP_ARGEO_LOGGING_CALL_LOCATION = "argeo.logging.callLocation"; + /** A file to which additionally write log entries. */ + final static String PROP_ARGEO_LOGGING_FILE = "argeo.logging.file"; + /** + * The level from which call location (that is, line number in Java code) will + * be searched (default is WARNING) + */ + final static String PROP_ARGEO_LOGGING_CALL_LOCATION_LEVEL = "argeo.logging.callLocationLevel"; final static String ENV_INVOCATION_ID = "INVOCATION_ID"; final static String ENV_GIO_LAUNCHED_DESKTOP_FILE_PID = "GIO_LAUNCHED_DESKTOP_FILE_PID"; @@ -49,49 +68,51 @@ class ThinLogging implements Consumer> { // 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 SortedMap loggers = Collections.synchronizedSortedMap(new TreeMap<>()); private NavigableMap levels = new TreeMap<>(); private volatile boolean updatingConfiguration = false; -// private final ExecutorService executor; private final LogEntryPublisher publisher; private PrintStreamSubscriber synchronousSubscriber; + private PrintStream fileOut; private final boolean journald; private final Level callLocationLevel; - private boolean synchronous = Boolean.parseBoolean(System.getProperty(PROP_ARGEO_LOGGING_SYNCHRONOUS)); + private final boolean synchronous; ThinLogging() { -// executor = Executors.newCachedThreadPool((r) -> { -// Thread t = new Thread(r); -// t.setDaemon(true); -// return t; -// }); + synchronous = Boolean.parseBoolean(System.getProperty(PROP_ARGEO_LOGGING_SYNCHRONOUS, "false")); if (synchronous) { - publisher = new LogEntryPublisher(); synchronousSubscriber = new PrintStreamSubscriber(); + publisher = null; } else { - publisher = new LogEntryPublisher(); - PrintStreamSubscriber subscriber = new PrintStreamSubscriber(); + publisher = new LogEntryPublisher(); publisher.subscribe(subscriber); - - Runtime.getRuntime().addShutdownHook(new Thread(() -> close(), "Log shutdown")); - + String logFileStr = System.getProperty(PROP_ARGEO_LOGGING_FILE); + if (logFileStr != null) { + Path logFilePath = Paths.get(logFileStr); + if (!Files.exists(logFilePath.getParent())) { + System.err.println("Parent directory of " + logFilePath + " does not exist"); + } else { + try { + fileOut = new PrintStream(Files.newOutputStream(logFilePath), true, StandardCharsets.UTF_8); + publisher.subscribe(new PrintStreamSubscriber(fileOut, fileOut)); + } catch (IOException e) { + System.err.println("Cannot write log to " + logFilePath); + e.printStackTrace(); + } + } + } } + Runtime.getRuntime().addShutdownHook(new Thread(() -> close(), "Log shutdown")); // initial default level levels.put(DEFAULT_LEVEL_NAME, Level.WARNING); // Logging system config // journald - -// Map env = new TreeMap<>(System.getenv()); -// for (String key : env.keySet()) { -// System.out.println(key + "=" + env.get(key)); -// } - String journaldStr = System.getProperty(PROP_ARGEO_LOGGING_JOURNALD, "auto"); switch (journaldStr) { case "auto": @@ -124,12 +145,12 @@ class ThinLogging implements Consumer> { "Unsupported value '" + journaldStr + "' for property " + PROP_ARGEO_LOGGING_JOURNALD); } - String callLocationStr = System.getProperty(PROP_ARGEO_LOGGING_CALL_LOCATION, Level.WARNING.getName()); + String callLocationStr = System.getProperty(PROP_ARGEO_LOGGING_CALL_LOCATION_LEVEL, Level.WARNING.getName()); callLocationLevel = Level.valueOf(callLocationStr); } private void close() { - RuntimeContext runtimeContext = Service.getRuntimeContext(); + RuntimeContext runtimeContext = InternalState.getMainRuntimeContext(); if (runtimeContext != null) { try { runtimeContext.waitForStop(0); @@ -138,14 +159,24 @@ class ThinLogging implements Consumer> { } } - publisher.close(); - try { - // we wait 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 + if (!synchronous) { + publisher.close(); + try { + // we wait 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 + } + } + + if (fileOut != null) { + try { + fileOut.close(); + } catch (Exception e) { + // silent + } } } @@ -160,9 +191,11 @@ class ThinLogging implements Consumer> { } public Logger getLogger(String name, Module module) { + Objects.requireNonNull(name, "logger name"); if (!loggers.containsKey(name)) { ThinLogger logger = new ThinLogger(name, computeApplicableLevel(name)); loggers.put(name, logger); + return logger; } return loggers.get(name); } @@ -221,6 +254,45 @@ class ThinLogging implements Consumer> { return Collections.unmodifiableNavigableMap(levels); } + private void dispatchLogEntry(ThinLogger logger, Level level, ResourceBundle bundle, String msg, Instant instant, + Thread thread, Throwable thrown, StackTraceElement callLocation) { + assert level != null; + assert logger != null; +// assert msg != null; + assert instant != null; + assert thread != null; + + if (msg == null) + msg = "null"; + + final long sequence = nextEntry.incrementAndGet(); + + Map logEntry = new LogEntryMap(sequence); + + // same object as key class name + logEntry.put(KEY_LEVEL, level); + logEntry.put(KEY_MSG, msg); + logEntry.put(KEY_INSTANT, instant); + if (thrown != null) + logEntry.put(KEY_THROWABLE, thrown); + if (callLocation != null) + logEntry.put(KEY_CALL_LOCATION, callLocation); + + // object is a string + logEntry.put(KEY_LOGGER, logger.getName()); + logEntry.put(KEY_THREAD, thread.getName()); + + // should be unmodifiable for security reasons + if (synchronous) { + assert synchronousSubscriber != null; + synchronousSubscriber.onNext(logEntry); + } else { + if (!publisher.isClosed()) + publisher.submit(Collections.unmodifiableMap(logEntry)); + } + + } + /* * INTERNAL CLASSES */ @@ -269,7 +341,7 @@ class ThinLogging implements Consumer> { // measure timestamp first Instant now = Instant.now(); Thread thread = Thread.currentThread(); - publisher.log(this, level, bundle, msg, now, thread, thrown, findCallLocation(level, thread)); + dispatchLogEntry(ThinLogger.this, level, bundle, msg, now, thread, thrown, findCallLocation(level, thread)); } @Override @@ -296,7 +368,8 @@ class ThinLogging implements Consumer> { 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)); + dispatchLogEntry(ThinLogger.this, level, bundle, msg, now, thread, (Throwable) null, + findCallLocation(level, thread)); } private void setLevel(Level level) { @@ -322,11 +395,11 @@ class ThinLogging implements Consumer> { 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": + case "org.slf4j.impl.ArgeoLogger": + case "org.argeo.cms.internal.osgi.CmsOsgiLogger": + case "org.argeo.init.osgi.OsgiBootUtils": lowestLoggerInterface = i; continue stack; default: @@ -354,40 +427,10 @@ class ThinLogging implements Consumer> { super(); } - private void log(ThinLogger logger, Level level, ResourceBundle bundle, String msg, Instant instant, - Thread thread, Throwable thrown, StackTraceElement callLocation) { - assert level != null; - assert logger != null; - assert msg != null; - assert instant != null; - assert thread != null; - - final long sequence = nextEntry.incrementAndGet(); - - Map logEntry = new LogEntryMap(sequence); - - // same object as key class name - logEntry.put(KEY_LEVEL, level); - logEntry.put(KEY_MSG, msg); - logEntry.put(KEY_INSTANT, instant); - if (thrown != null) - logEntry.put(KEY_THROWABLE, thrown); - if (callLocation != null) - logEntry.put(KEY_CALL_LOCATION, callLocation); - - // object is a string - logEntry.put(KEY_LOGGER, logger.getName()); - logEntry.put(KEY_THREAD, thread.getName()); - - // should be unmodifiable for security reasons - if (synchronous) { - assert synchronousSubscriber != null; - synchronousSubscriber.onNext(logEntry); - } else { - if (!isClosed()) - submit(Collections.unmodifiableMap(logEntry)); - } - } +// private void log(ThinLogger logger, Level level, ResourceBundle bundle, String msg, Instant instant, +// Thread thread, Throwable thrown, StackTraceElement callLocation) { +// +// } } @@ -462,7 +505,8 @@ class ThinLogging implements Consumer> { out.print(toPrint(item)); } // TODO flush for journald? - this.subscription.request(1); + if (this.subscription != null) + this.subscription.request(1); } @Override @@ -567,24 +611,24 @@ class ThinLogging implements Consumer> { } } - public static void main(String args[]) { - Logger logger = System.getLogger(ThinLogging.class.getName()); - logger.log(Logger.Level.ALL, "Log all"); - logger.log(Logger.Level.TRACE, "Multi\nline\ntrace"); - logger.log(Logger.Level.DEBUG, "Log debug"); - 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 - } - - } +// public static void main(String args[]) { +// Logger logger = System.getLogger(ThinLogging.class.getName()); +// logger.log(Logger.Level.ALL, "Log all"); +// logger.log(Logger.Level.TRACE, "Multi\nline\ntrace"); +// logger.log(Logger.Level.DEBUG, "Log debug"); +// 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 wait 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 +// } +// +// } }