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.
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);
private NavigableMap<String, Level> 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
// 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)) {
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
}
private Level computeApplicableLevel(String name) {
Map.Entry<String, Level> 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));
updatingConfiguration = false;
levels.notifyAll();
}
-
}
Flow.Publisher<Map<String, Serializable>> getLogEntryPublisher() {
/*
* INTERNAL CLASSES
*/
-
+
private class ThinLogger implements System.Logger {
private final String name;
// 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));
}
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();
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.argeo.cms.Log":
+ 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;
continue stack;
default:
private class LogEntryPublisher extends SubmissionPublisher<Map<String, Serializable>> {
- 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,
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));
+ }
}
}
private PrintStream err;
private int writeToErrLevel = Level.WARNING.getSeverity();
+ private Subscription subscription;
+
protected PrintStreamSubscriber() {
this(System.out, System.err);
}
@Override
public void onSubscribe(Subscription subscription) {
- subscription.request(Long.MAX_VALUE);
+ this.subscription = subscription;
+ this.subscription.request(1);
}
@Override
out.print(toPrint(item));
}
// TODO flush for journald?
+ this.subscription.request(1);
}
@Override
protected String toPrint(Map<String, Serializable> 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));
sb.append('\n');
for (StackTraceElement ste : throwable.getStackTrace()) {
sb.append(prefix);
+ sb.append('\t');
sb.append(ste.toString());
sb.append('\n');
}
if (throwable.getCause() != null) {
sb.append(prefix);
- sb.append("caused by ");
+ sb.append("Caused by: ");
addThrowable(sb, prefix, throwable.getCause());
}
}
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
+ }
+
}
}