diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/config/BlueMapConfigs.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/config/BlueMapConfigs.java index ad3f640d..7905d053 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/config/BlueMapConfigs.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/config/BlueMapConfigs.java @@ -65,7 +65,7 @@ public class BlueMapConfigs implements BlueMapConfigProvider { this.coreConfig = loadCoreConfig(defaultDataFolder); this.webappConfig = loadWebappConfig(defaultWebroot); - this.webserverConfig = loadWebserverConfig(webappConfig.getWebroot()); + this.webserverConfig = loadWebserverConfig(webappConfig.getWebroot(), coreConfig.getData()); this.pluginConfig = usePluginConf ? loadPluginConfig() : new PluginConfig(); this.storageConfigs = Collections.unmodifiableMap(loadStorageConfigs(webappConfig.getWebroot())); this.mapConfigs = Collections.unmodifiableMap(loadMapConfigs()); @@ -144,7 +144,7 @@ public class BlueMapConfigs implements BlueMapConfigProvider { return configManager.loadConfig(configFileRaw, CoreConfig.class); } - private synchronized WebserverConfig loadWebserverConfig(Path defaultWebroot) throws ConfigurationException { + private synchronized WebserverConfig loadWebserverConfig(Path defaultWebroot, Path dataRoot) throws ConfigurationException { Path configFileRaw = Path.of("webserver"); Path configFile = configManager.findConfigPath(configFileRaw); Path configFolder = configFile.getParent(); @@ -156,6 +156,8 @@ public class BlueMapConfigs implements BlueMapConfigProvider { configFolder.resolve("webserver.conf"), configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/webserver.conf") .setVariable("webroot", formatPath(defaultWebroot)) + .setVariable("logfile", formatPath(dataRoot.resolve("logs").resolve("webapp.log"))) + .setVariable("logfile-with-time", formatPath(dataRoot.resolve("logs").resolve("webapp_%1$tF_%1$tT.log"))) .build(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING ); diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/config/WebserverConfig.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/config/WebserverConfig.java index 20631ca3..c2f4e9d5 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/config/WebserverConfig.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/config/WebserverConfig.java @@ -38,13 +38,13 @@ import java.nio.file.Path; public class WebserverConfig { private boolean enabled = true; - private Path webroot = Path.of("bluemap", "web"); private String ip = "0.0.0.0"; - private int port = 8100; + private LogConfig log = new LogConfig(); + public boolean isEnabled() { return enabled; } @@ -71,4 +71,30 @@ public class WebserverConfig { return port; } + public LogConfig getLog() { + return log; + } + + @DebugDump + @ConfigSerializable + public static class LogConfig { + + private String file = null; + private boolean append = true; + private String format = "%1$s \"%3$s %4$s %5$s\" %6$s %7$s"; + + public String getFile() { + return file; + } + + public boolean isAppend() { + return append; + } + + public String getFormat() { + return format; + } + + } + } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java index 42b58a81..d444a727 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java @@ -59,6 +59,9 @@ import java.net.BindException; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; @@ -185,8 +188,22 @@ public class Plugin implements ServerEventListener { ); } + // create web-logger + List webLoggerList = new ArrayList<>(); + if (webserverConfig.getLog().getFile() != null) { + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()); + webLoggerList.add(Logger.file( + Path.of(String.format(webserverConfig.getLog().getFile(), zdt)), + webserverConfig.getLog().isAppend() + )); + } + try { - webServer = new HttpServer(routingRequestHandler); + webServer = new HttpServer(new LoggingRequestHandler( + routingRequestHandler, + webserverConfig.getLog().getFormat(), + Logger.combine(webLoggerList) + )); webServer.bind(new InetSocketAddress( webserverConfig.resolveIp(), webserverConfig.getPort() diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/LoggingRequestHandler.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/LoggingRequestHandler.java index 386382e8..3b759d50 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/LoggingRequestHandler.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/LoggingRequestHandler.java @@ -1,50 +1,74 @@ package de.bluecolored.bluemap.common.web; -import de.bluecolored.bluemap.common.web.http.HttpHeader; -import de.bluecolored.bluemap.common.web.http.HttpRequest; -import de.bluecolored.bluemap.common.web.http.HttpRequestHandler; -import de.bluecolored.bluemap.common.web.http.HttpResponse; +import de.bluecolored.bluemap.common.web.http.*; import de.bluecolored.bluemap.core.logger.Logger; public class LoggingRequestHandler implements HttpRequestHandler { private final HttpRequestHandler delegate; private final Logger logger; + private final String format; public LoggingRequestHandler(HttpRequestHandler delegate) { this(delegate, Logger.global); } public LoggingRequestHandler(HttpRequestHandler delegate, Logger logger) { + this(delegate, "", logger); + } + + public LoggingRequestHandler(HttpRequestHandler delegate, String format) { + this(delegate, format, Logger.global); + } + + public LoggingRequestHandler(HttpRequestHandler delegate, String format, Logger logger) { this.delegate = delegate; + this.format = format; this.logger = logger; } @Override public HttpResponse handle(HttpRequest request) { - String source = request.getSource().toString(); + // gather format parameters from request + String source = request.getSource().toString(); + String xffSource = source; HttpHeader xffHeader = request.getHeader("X-Forwarded-For"); if (xffHeader != null && !xffHeader.getValues().isEmpty()) { - source = xffHeader.getValues().get(0); + xffSource = xffHeader.getValues().get(0); } - StringBuilder log = new StringBuilder() - .append(source) - .append(" \"").append(request.getMethod()) - .append(" ").append(request.getAddress()) - .append(" ").append(request.getVersion()) - .append("\" "); + String method = request.getMethod(); + String address = request.getAddress(); + String version = request.getVersion(); + // run request HttpResponse response = delegate.handle(request); - log.append(response.getStatusCode()); - if (response.getStatusCode().getCode() < 400) { - logger.logInfo(log.toString()); + // gather format parameters from response + HttpStatusCode status = response.getStatusCode(); + int statusCode = status.getCode(); + String statusMessage = status.getMessage(); + + // format log message + String log = String.format(this.format, + source, + xffSource, + method, + address, + version, + statusCode, + statusMessage + ); + + // do the logging + if (statusCode < 500) { + logger.logInfo(log); } else { - logger.logWarning(log.toString()); + logger.logWarning(log); } + // return the response return response; } diff --git a/BlueMapCommon/src/main/resources/de/bluecolored/bluemap/config/webserver.conf b/BlueMapCommon/src/main/resources/de/bluecolored/bluemap/config/webserver.conf index 5332e952..78c22d7c 100644 --- a/BlueMapCommon/src/main/resources/de/bluecolored/bluemap/config/webserver.conf +++ b/BlueMapCommon/src/main/resources/de/bluecolored/bluemap/config/webserver.conf @@ -16,3 +16,30 @@ webroot: "${webroot}" # The port that the webserver listens to. # Default is 8100 port: 8100 + +# Config-section for webserver-activity logging +log: { + # The file where all the webserver-activity will be logged to. + # Comment out to disable the logging completely. + # Java String formatting syntax can be used to add time + # Default is no logging + file: "${logfile}" + #file: "${logfile-with-time}" + + # Whether the logger should append to an existing file, or overwrite it + # Default is true + append: true + + # The format of the webserver-acivity logs. + # The syntax is the java String formatting, see: https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html + # Possible Arguments: + # 1 - the source address (ignoring any xff headers) + # 2 - the source address (using the (leftmost) xff header if provided) + # 3 - the http-method of the request + # 4 - the full request-address + # 5 - the protocol version of the request + # 6 - the status-code of the response + # 7 - the status-message of the response + # Default is "%1$s \"%3$s %4$s %5$s\" %6$s %7$s" + format: "%1$s \"%3$s %4$s %5$s\" %6$s %7$s" +} diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/JavaLogger.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/JavaLogger.java new file mode 100644 index 00000000..e72134da --- /dev/null +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/JavaLogger.java @@ -0,0 +1,44 @@ +package de.bluecolored.bluemap.core.logger; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class JavaLogger extends AbstractLogger { + + private final Logger out; + + public JavaLogger(Logger out) { + this.out = out; + } + + @Override + public void logError(String message, Throwable throwable) { + out.log(Level.SEVERE, message, throwable); + } + + @Override + public void logWarning(String message) { + out.log(Level.WARNING, message); + } + + @Override + public void logInfo(String message) { + out.log(Level.INFO, message); + } + + @Override + public void logDebug(String message) { + if (out.isLoggable(Level.FINE)) out.log(Level.FINE, message); + } + + @Override + public void noFloodDebug(String message) { + if (out.isLoggable(Level.FINE)) super.noFloodDebug(message); + } + + @Override + public void noFloodDebug(String key, String message) { + if (out.isLoggable(Level.FINE)) super.noFloodDebug(key, message); + } + +} diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/LogFormatter.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/LogFormatter.java new file mode 100644 index 00000000..eb0fa18a --- /dev/null +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/LogFormatter.java @@ -0,0 +1,58 @@ +package de.bluecolored.bluemap.core.logger; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +public class LogFormatter extends Formatter { + + private final String format; + + public LogFormatter() { + this("[%1$tF %1$tT][%4$s] %5$s%6$s%n"); + } + + public LogFormatter(String format) { + this.format = format; + } + + /** + * Taken from {@link java.util.logging.SimpleFormatter} to be able to overwrite the format string. + * @param record the log record to be formatted. + * @return the formatted log + */ + public String format(LogRecord record) { + ZonedDateTime zdt = ZonedDateTime.ofInstant( + record.getInstant(), ZoneId.systemDefault()); + String source; + if (record.getSourceClassName() != null) { + source = record.getSourceClassName(); + if (record.getSourceMethodName() != null) { + source += " " + record.getSourceMethodName(); + } + } else { + source = record.getLoggerName(); + } + String message = formatMessage(record); + String throwable = ""; + if (record.getThrown() != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + pw.println(); + record.getThrown().printStackTrace(pw); + pw.close(); + throwable = sw.toString(); + } + return String.format(format, + zdt, + source, + record.getLoggerName(), + record.getLevel().getLocalizedName(), + message, + throwable); + } + +} diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/Logger.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/Logger.java index 1bc457b0..5ae2c2e1 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/Logger.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/Logger.java @@ -24,6 +24,14 @@ */ package de.bluecolored.bluemap.core.logger; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.logging.FileHandler; +import java.util.stream.StreamSupport; + public abstract class Logger { public static Logger global = stdOut(); @@ -107,4 +115,41 @@ public abstract class Logger { return new PrintStreamLogger(System.out, System.err); } + + public static Logger file(Path path) throws IOException { + return file(path, null); + } + + public static Logger file(Path path, boolean append) throws IOException { + return file(path, null, append); + } + + public static Logger file(Path path, String format) throws IOException { + return file(path, format, true); + } + + public static Logger file(Path path, @Nullable String format, boolean append) throws IOException { + Files.createDirectories(path.getParent()); + + FileHandler fileHandler = new FileHandler(path.toString(), append); + fileHandler.setFormatter(format == null ? new LogFormatter() : new LogFormatter(format)); + + java.util.logging.Logger javaLogger = java.util.logging.Logger.getAnonymousLogger(); + javaLogger.setUseParentHandlers(false); + javaLogger.addHandler(fileHandler); + + return new JavaLogger(javaLogger); + } + + public static Logger combine(Iterable logger) { + return combine(StreamSupport.stream(logger.spliterator(), false) + .toArray(Logger[]::new)); + } + + public static Logger combine(Logger... logger) { + if (logger.length == 0) return new VoidLogger(); + if (logger.length == 1) return logger[0]; + return new MultiLogger(logger); + } + } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/MultiLogger.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/MultiLogger.java new file mode 100644 index 00000000..1c73c2c0 --- /dev/null +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/logger/MultiLogger.java @@ -0,0 +1,36 @@ +package de.bluecolored.bluemap.core.logger; + +@SuppressWarnings("ForLoopReplaceableByForEach") +public class MultiLogger extends AbstractLogger { + + private final Logger[] logger; + + public MultiLogger(Logger... logger) { + this.logger = logger; + } + + @Override + public void logError(String message, Throwable throwable) { + for (int i = 0; i < logger.length; i++) + logger[i].logError(message, throwable); + } + + @Override + public void logWarning(String message) { + for (int i = 0; i < logger.length; i++) + logger[i].logWarning(message); + } + + @Override + public void logInfo(String message) { + for (int i = 0; i < logger.length; i++) + logger[i].logInfo(message); + } + + @Override + public void logDebug(String message) { + for (int i = 0; i < logger.length; i++) + logger[i].logDebug(message); + } + +} diff --git a/implementations/cli/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java b/implementations/cli/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java index 55dc7909..268edbe8 100644 --- a/implementations/cli/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java +++ b/implementations/cli/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java @@ -58,6 +58,9 @@ import java.net.BindException; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @@ -203,10 +206,25 @@ public class BlueMapCLI implements ServerInterface { ); } + List webLoggerList = new ArrayList<>(); + if (verbose) webLoggerList.add(Logger.global); + if (config.getLog().getFile() != null) { + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()); + webLoggerList.add(Logger.file( + Path.of(String.format(config.getLog().getFile(), zdt)), + config.getLog().isAppend() + )); + } + HttpRequestHandler handler = new BlueMapResponseModifier(routingRequestHandler); - if (verbose) handler = new LoggingRequestHandler(handler); + handler = new LoggingRequestHandler( + handler, + config.getLog().getFormat(), + Logger.combine(webLoggerList) + ); try { + //noinspection resource HttpServer webServer = new HttpServer(handler); webServer.bind(new InetSocketAddress( config.resolveIp(),