diff --git a/common/src/main/java/com/discordsrv/common/config/main/ConsoleConfig.java b/common/src/main/java/com/discordsrv/common/config/main/ConsoleConfig.java new file mode 100644 index 00000000..4be21ab6 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/config/main/ConsoleConfig.java @@ -0,0 +1,60 @@ +package com.discordsrv.common.config.main; + +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.Locale; + +public class ConsoleConfig { + + @Comment("The format for log lines") + public String lineFormat = "[%log_time:'ccc HH:mm:ss zzz'%] [%log_level%] [%logger_name%] %message%"; + + @Comment("The mode for the console output, available options are:\n" + + "- ansi: A colored ansi code block\n" + + "- log: A \"accesslog\" code block\n" + + "- diff: A \"diff\" code block highlighting warnings and errors with different colors\n" + + "- plain: Plain text code block\n" + + "- plain_content: Plain text") + public String outputMode = "ansi"; + + public OutputMode getOutputMode() { + switch (outputMode.toLowerCase(Locale.ROOT)) { + default: + case "ansi": return OutputMode.ANSI; + case "log": return OutputMode.LOG; + case "diff": return OutputMode.DIFF; + case "plain": return OutputMode.PLAIN; + case "plain_content": return OutputMode.PLAIN_CONTENT; + } + } + + @Comment("Avoids sending new messages by editing the most recent message until it reaches it's maximum length") + public boolean useEditing = true; + + @Comment("If console messages should be silent, not causing a notification") + public boolean silentMessages = true; + + public enum OutputMode { + ANSI("```ansi\n", "```"), + LOG("```accesslog\n", "```"), + DIFF("```diff\n", "```"), + PLAIN("```\n", "```"), + PLAIN_CONTENT("", ""); + + private final String prefix; + private final String suffix; + + OutputMode(String prefix, String suffix) { + this.prefix = prefix; + this.suffix = suffix; + } + + public String prefix() { + return prefix; + } + + public String suffix() { + return suffix; + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/console/ConsoleModule.java b/common/src/main/java/com/discordsrv/common/console/ConsoleModule.java new file mode 100644 index 00000000..98b7cf73 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/console/ConsoleModule.java @@ -0,0 +1,58 @@ +package com.discordsrv.common.console; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.config.main.ConsoleConfig; +import com.discordsrv.common.console.entry.LogEntry; +import com.discordsrv.common.logging.LogAppender; +import com.discordsrv.common.logging.LogLevel; +import com.discordsrv.common.logging.NamedLogger; +import com.discordsrv.common.logging.backend.LoggingBackend; +import com.discordsrv.common.module.type.AbstractModule; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class ConsoleModule extends AbstractModule implements LogAppender { + + private LoggingBackend backend; + private final List handlers = new ArrayList<>(); + + public ConsoleModule(DiscordSRV discordSRV) { + super(discordSRV, new NamedLogger(discordSRV, "CONSOLE")); + } + + @Override + public void enable() { + backend = discordSRV.console().loggingBackend(); + backend.addAppender(this); + + reload(); + } + + @Override + public void reloadNoResult() { + handlers.add(new SingleConsoleHandler(discordSRV, new ConsoleConfig())); // TODO + } + + @Override + public void disable() { + if (backend != null) { + backend.removeAppender(this); + } + } + + @Override + public void append( + @Nullable String loggerName, + @NotNull LogLevel logLevel, + @Nullable String message, + @Nullable Throwable throwable + ) { + LogEntry entry = new LogEntry(loggerName, logLevel, message, throwable); + for (SingleConsoleHandler handler : handlers) { + handler.queue(entry); + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/console/SingleConsoleHandler.java b/common/src/main/java/com/discordsrv/common/console/SingleConsoleHandler.java new file mode 100644 index 00000000..63fc16e0 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/console/SingleConsoleHandler.java @@ -0,0 +1,125 @@ +package com.discordsrv.common.console; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.config.main.ConsoleConfig; +import com.discordsrv.common.console.entry.LogEntry; +import com.discordsrv.common.console.entry.LogMessage; +import com.discordsrv.common.logging.LogLevel; +import net.dv8tion.jda.api.entities.Message; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +public class SingleConsoleHandler { + + private static final int MESSAGE_MAX_LENGTH = Message.MAX_CONTENT_LENGTH; + + private final DiscordSRV discordSRV; + private final ConsoleConfig config; + private final Queue queue = new LinkedBlockingQueue<>(); + private final List messageCache; + + public SingleConsoleHandler(DiscordSRV discordSRV, ConsoleConfig config) { + this.discordSRV = discordSRV; + this.config = config; + this.messageCache = config.useEditing ? new ArrayList<>() : null; + } + + public void queue(LogEntry entry) { + queue.offer(entry); + } + + public void shutdown() { + + } + + private void processQueue() { + ConsoleConfig.OutputMode outputMode = config.getOutputMode(); + + + } + + private List formatEntry(LogEntry entry) { + String message = entry.message(); + String throwable = ExceptionUtils.getMessage(entry.throwable()); + + message = discordSRV.placeholderService().replacePlaceholders(config.lineFormat, entry); + + ConsoleConfig.OutputMode outputMode = config.getOutputMode(); + + String prefix = outputMode.prefix(); + String suffix = outputMode.suffix(); + int blockLength = prefix.length() + suffix.length(); + int maximumPart = MESSAGE_MAX_LENGTH - blockLength - "\n".length(); + + if (outputMode == ConsoleConfig.OutputMode.DIFF) { + message = getLogLevelDiffCharacter(entry.level()) + message; + // TODO: also format throwable? + } + + message += "\n"; + if (!throwable.isEmpty()) { + throwable += "\n"; + } + + List formatted = new ArrayList<>(); + + // Handle message being longer than a message + message = cutToSizeIfNeeded(message, blockLength, maximumPart, formatted); + + // Handle log entry being longer than a message + int totalLength = blockLength + throwable.length() + message.length(); + if (totalLength > MESSAGE_MAX_LENGTH) { + StringBuilder builder = new StringBuilder(message); + for (String line : throwable.split("\n")) { + line += "\n"; + + // Handle a single line of a throwable being longer than a message + line = cutToSizeIfNeeded(line, blockLength, maximumPart, formatted); + + if (blockLength + line.length() > MESSAGE_MAX_LENGTH) { + // Need to split here + formatted.add(builder.toString()); + builder.setLength(0); + } + builder.append(line); + } + formatted.add(builder.toString()); + } else { + formatted.add(message + throwable); + } + + return formatted; + } + + private String cutToSizeIfNeeded( + String content, + int blockLength, + int maximumPart, + List formatted + ) { + while (content.length() + blockLength > MESSAGE_MAX_LENGTH) { + String cutToSize = content.substring(0, maximumPart) + "\n"; + if (cutToSize.endsWith("\n\n")) { + // maximumPart excludes the newline at the end of message/line + cutToSize = cutToSize.substring(0, cutToSize.length() - 1); + } + + formatted.add(cutToSize); + content = content.substring(maximumPart); + } + return content; + } + + private String getLogLevelDiffCharacter(LogLevel level) { + if (level == LogLevel.StandardLogLevel.WARNING) { + return "+"; + } else if (level == LogLevel.StandardLogLevel.ERROR) { + return "-"; + } + return " "; + } +} diff --git a/common/src/main/java/com/discordsrv/common/console/entry/LogEntry.java b/common/src/main/java/com/discordsrv/common/console/entry/LogEntry.java new file mode 100644 index 00000000..6b370333 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/console/entry/LogEntry.java @@ -0,0 +1,61 @@ +package com.discordsrv.common.console.entry; + +import com.discordsrv.api.placeholder.PlaceholderLookupResult; +import com.discordsrv.api.placeholder.annotation.Placeholder; +import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder; +import com.discordsrv.common.logging.LogLevel; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashSet; +import java.util.Set; + +public class LogEntry { + + @Placeholder("logger_name") + private final String loggerName; + @Placeholder("log_level") + private final LogLevel level; + private final String message; + private final Throwable throwable; + private final Instant logTime; + + public LogEntry(String loggerName, LogLevel level, String message, Throwable throwable) { + this.loggerName = loggerName; + this.level = level; + this.message = message; + this.throwable = throwable; + this.logTime = Instant.now(); + } + + public String loggerName() { + return loggerName; + } + + public LogLevel level() { + return level; + } + + public String message() { + return message; + } + + public Throwable throwable() { + return throwable; + } + + public Instant logTime() { + return logTime; + } + + @Placeholder("log_time") + public PlaceholderLookupResult _logTimePlaceholder(@PlaceholderRemainder String format) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format); // TODO: cache + + Set extras = new LinkedHashSet<>(); + extras.add(formatter); + extras.add(logTime); + + return PlaceholderLookupResult.newLookup("date", extras); + } +} diff --git a/common/src/main/java/com/discordsrv/common/console/entry/LogMessage.java b/common/src/main/java/com/discordsrv/common/console/entry/LogMessage.java new file mode 100644 index 00000000..fa11cdbf --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/console/entry/LogMessage.java @@ -0,0 +1,19 @@ +package com.discordsrv.common.console.entry; + +public class LogMessage { + + private final LogEntry entry; + private String formatted; + + public LogMessage(LogEntry entry) { + this.entry = entry; + } + + public String getFormatted() { + return formatted; + } + + public void setFormatted(String formatted) { + this.formatted = formatted; + } +} diff --git a/common/src/main/java/com/discordsrv/common/logging/backend/impl/Log4JLoggerImpl.java b/common/src/main/java/com/discordsrv/common/logging/backend/impl/Log4JLoggerImpl.java index 5a942e2d..419867fe 100644 --- a/common/src/main/java/com/discordsrv/common/logging/backend/impl/Log4JLoggerImpl.java +++ b/common/src/main/java/com/discordsrv/common/logging/backend/impl/Log4JLoggerImpl.java @@ -149,8 +149,8 @@ public class Log4JLoggerImpl implements Logger, LoggingBackend { public boolean removeAppender(LogAppender appender) { if (logger instanceof org.apache.logging.log4j.core.Logger) { org.apache.logging.log4j.core.Logger loggerImpl = (org.apache.logging.log4j.core.Logger) logger; - Appender log4jAppender = appenders.get(appender); - loggerImpl.addAppender(log4jAppender); + Appender log4jAppender = appenders.remove(appender); + loggerImpl.removeAppender(log4jAppender); return true; } return false;