Logger.java
package com.renomad.minum.logging;
import com.renomad.minum.state.Constants;
import com.renomad.minum.queue.AbstractActionQueue;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import static com.renomad.minum.utils.TimeUtils.getTimestampIsoInstant;
/**
* Implementation of {@link ILogger}
*/
public class Logger implements ILogger {
/**
* The {@link LoggingActionQueue} that handles all
* our messages thread-safely by taking
* them off the top of a queue.
*/
protected final AbstractActionQueue loggingActionQueue;
private final Constants constants;
private final ExecutorService executorService;
private final String name;
private final Map<LoggingLevel, Boolean> activeLogLevels;
/**
* Constructor
* @param constants used for determining enabled log levels
* @param executorService provides thread handling for the logs, used to
* build a {@link LoggingActionQueue}
* @param name sets a name on the {@link LoggingActionQueue} to aid debugging, to
* help distinguish queues.
*/
public Logger(Constants constants, ExecutorService executorService, String name) {
this(constants, executorService, name, null);
}
private Logger(Constants constants, ExecutorService executorService, String name, AbstractActionQueue loggingActionQueue) {
this.constants = constants;
this.executorService = executorService;
this.name = name;
// this tricky code exists so that a user has the option to create a class extended
// from this one, and can construct it with the logger instance, making it possible
// to inject the running action queue. This enables us to continue using the same
// action queue amongst descendant classes.
if (loggingActionQueue == null) {
this.loggingActionQueue = new LoggingActionQueue("loggerPrinter" + name, executorService, constants).initialize();
} else {
this.loggingActionQueue = loggingActionQueue;
}
activeLogLevels = convertToMap(constants.logLevels);
}
/**
* A constructor meant for use by descendant classes
* @param logger an existing instance of a running logger, needed in order to have the
* descendant logger using the same {@link AbstractActionQueue}, which is
* necessary so logs don't interleave with each other.
*/
public Logger(Logger logger) {
this(logger.constants, logger.executorService, logger.name, logger.loggingActionQueue);
}
/**
* Convert the list of enabled log levels to a map of enum -> boolean
*/
static Map<LoggingLevel, Boolean> convertToMap(List<LoggingLevel> enabledLoggingLevels) {
Map<LoggingLevel, Boolean> activeLogLevels = new EnumMap<>(LoggingLevel.class);
for (LoggingLevel t : LoggingLevel.values()) {
activeLogLevels.put(t, enabledLoggingLevels.contains(t));
}
return activeLogLevels;
}
@Override
public void logDebug(ThrowingSupplier<String, Exception> msg) {
logHelper(msg, LoggingLevel.DEBUG, activeLogLevels, loggingActionQueue);
}
@Override
public void logTrace(ThrowingSupplier<String, Exception> msg) {
logHelper(msg, LoggingLevel.TRACE, activeLogLevels, loggingActionQueue);
}
@Override
public void logAudit(ThrowingSupplier<String, Exception> msg) {
logHelper(msg, LoggingLevel.AUDIT, activeLogLevels, loggingActionQueue);
}
@Override
public void stop() {
this.loggingActionQueue.stop();
this.executorService.shutdownNow();
}
@Override
public void logAsyncError(ThrowingSupplier<String, Exception> msg) {
logHelper(msg, LoggingLevel.ASYNC_ERROR, activeLogLevels, loggingActionQueue);
}
@Override
public Map<LoggingLevel, Boolean> getActiveLogLevels() {
return activeLogLevels;
}
/**
* A helper method to reduce duplication
*/
static void logHelper(
ThrowingSupplier<String, Exception> msg,
LoggingLevel loggingLevel,
Map<LoggingLevel, Boolean> activeLogLevels,
AbstractActionQueue loggingActionQueue
) {
if (Boolean.TRUE.equals(activeLogLevels.get(loggingLevel))) {
String receivedMessage;
try {
receivedMessage = msg.get();
} catch (Exception ex) {
receivedMessage = "EXCEPTION DURING GET: " + ex;
}
String finalReceivedMessage = receivedMessage;
if (loggingActionQueue == null || loggingActionQueue.isStopped()) {
Object[] args = new Object[]{getTimestampIsoInstant(), loggingLevel.name(), showWhiteSpace(finalReceivedMessage)};
System.out.printf("%s\t%s\t%s%n", args);
} else {
loggingActionQueue.enqueue("Logger#logHelper(" + receivedMessage + ")", () -> {
Object[] args = new Object[]{getTimestampIsoInstant(), loggingLevel.name(), showWhiteSpace(finalReceivedMessage)};
System.out.printf("%s\t%s\t%s%n", args);
});
}
}
}
/**
* Given a string that may have whitespace chars, render it in a way we can see
*/
static String showWhiteSpace(String msg) {
if (msg == null) return "(NULL)";
if (msg.isEmpty()) return "(EMPTY)";
// if we have tabs, returns, newlines in the text, show them
String text = msg
.replace("\t", "\\t")
.replace("\r", "\\r")
.replace("\n", "\\n");
if (text.isBlank()) return "(BLANK)";
return text;
}
}