diff --git a/server/core/src/main/java/io/eiren/util/logging/FileLogHandler.java b/server/core/src/main/java/io/eiren/util/logging/FileLogHandler.java new file mode 100644 index 0000000000..eee14f4e28 --- /dev/null +++ b/server/core/src/main/java/io/eiren/util/logging/FileLogHandler.java @@ -0,0 +1,265 @@ +package io.eiren.util.logging; + +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.logging.ErrorManager; +import java.util.logging.LogRecord; +import java.util.logging.StreamHandler; + + +public class FileLogHandler extends StreamHandler { + + protected class DatedLogFile implements Comparable { + public final File file; + public final LocalDateTime dateTime; + public final int count; + + protected DatedLogFile(File file, LocalDateTime dateTime, int count) { + this.file = file; + this.dateTime = dateTime; + this.count = count; + } + + @Override + public int compareTo(@NotNull DatedLogFile o) { + int dtCompare = dateTime.compareTo(o.dateTime); + return dtCompare != 0 ? dtCompare : Integer.compare(count, o.count); + } + } + + private final char sectionSeparator = '_'; + private final String logSuffix = ".log"; + + private final ArrayList logFiles; + + private final Path path; + private final String logTag; + private final DateTimeFormatter dateFormat; + private final LocalDateTime dateTime; + private final String date; + private final int limit; + private final int maxCount; + private final long collectiveLimit; + + private DataOutputStream curStream; + private int fileCount = 0; + private long collectiveSize = 0; + + public FileLogHandler( + @NotNull Path path, + @NotNull String logTag, + @NotNull DateTimeFormatter dateFormat, + int limit, + int count + ) { + this(path, logTag, dateFormat, limit, count, -1); + } + + public FileLogHandler( + @NotNull Path path, + @NotNull String logTag, + @NotNull DateTimeFormatter dateFormat, + int limit, + int count, + long collectiveLimit + ) { + this.path = path; + this.logTag = logTag; + + this.dateFormat = dateFormat; + this.dateTime = LocalDateTime.now(); + this.date = dateTime.format(dateFormat); + + this.limit = limit; + this.maxCount = count; + this.collectiveLimit = collectiveLimit; + + // Find old logs to manage + logFiles = findLogs(path); + if (collectiveLimit > 0) { + collectiveSize = sumFileSizes(logFiles); + } + + // Create new log and delete over the count + newFile(); + } + + private DatedLogFile parseFileName(File file) { + String name = file.getName(); + + // Log name should have at least two separators, one integer, and at + // least one char for the datetime (4 chars) + if ( + !name.startsWith(logTag) + || !name.endsWith(logSuffix) + || name.length() < (logTag.length() + logSuffix.length() + 4) + ) { + // Ignore non-matching files + return null; + } + + int dateEnd = name.lastIndexOf(sectionSeparator); + if (dateEnd < 0) { + // Ignore non-matching files + return null; + } + + try { + // Move past the tag, then between the two separators + String dateTimeStr = name.substring(logTag.length() + 1, dateEnd); + LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr, dateFormat); + + // Move past the date separator and behind the suffix + int logNum = Integer + .parseInt(name, dateEnd + 1, name.length() - logSuffix.length(), 10); + + return new DatedLogFile(file, dateTime, logNum); + } catch (Exception e) { + // Unable to parse log file, probably not valid + return null; + } + } + + private ArrayList findLogs(Path path) { + ArrayList logFiles = new ArrayList<>(); + + File[] files = path.toFile().listFiles(); + if (files == null) + return logFiles; + + // Find all parseable log files + for (File log : files) { + DatedLogFile parsedFile = parseFileName(log); + if (parsedFile != null) { + logFiles.add(parsedFile); + } + } + + return logFiles; + } + + private long sumFileSizes(ArrayList logFiles) { + long size = 0; + for (DatedLogFile log : logFiles) { + size += log.file.length(); + } + return size; + } + + private void deleteFile(File file) { + if (!file.delete()) { + file.deleteOnExit(); + reportError( + "Failed to delete file, deleting on exit.", + null, + ErrorManager.GENERIC_FAILURE + ); + } + } + + private DatedLogFile getEarliestFile(ArrayList logFiles) { + DatedLogFile earliest = null; + + for (DatedLogFile log : logFiles) { + if (earliest == null || log.compareTo(earliest) < 0) { + earliest = log; + } + } + + return earliest; + } + + private synchronized void deleteEarliestFile() { + DatedLogFile earliest = getEarliestFile(logFiles); + if (earliest != null) { + // If we have a collective limit, update the current size and clamp + if (collectiveLimit > 0) { + collectiveSize -= earliest.file.length(); + if (collectiveSize < 0) + collectiveSize = 0; + } + + logFiles.remove(earliest); + deleteFile(earliest.file); + } + } + + private synchronized void newFile() { + // Clear the last log file + if (curStream != null) { + collectiveSize += curStream.size(); + close(); + } + + if (maxCount > 0) { + // Delete files over the count + while (logFiles.size() >= maxCount) { + deleteEarliestFile(); + } + } + + if (collectiveLimit > 0) { + // Delete files over the collective size limit + while (!logFiles.isEmpty() && collectiveSize >= collectiveLimit) { + deleteEarliestFile(); + } + } + + try { + Path logPath = path + .resolve( + logTag + + sectionSeparator + + date + + sectionSeparator + + fileCount + + logSuffix + ); + File newFile = logPath.toFile(); + + // Use DataOutputStream to count bytes written + curStream = new DataOutputStream( + new BufferedOutputStream(new FileOutputStream(newFile)) + ); + // Closes the last stream automatically if not already done + setOutputStream(curStream); + + // Add log to the tracking list to be deleted if needed + logFiles.add(new DatedLogFile(newFile, dateTime, fileCount)); + fileCount += 1; + } catch (FileNotFoundException e) { + reportError(null, e, ErrorManager.OPEN_FAILURE); + } + } + + @Override + public synchronized void publish(LogRecord record) { + if (!isLoggable(record)) { + return; + } + + super.publish(record); + flush(); + + if (collectiveLimit > 0) { + // Delete files over the collective size limit + while (!logFiles.isEmpty() && collectiveSize + curStream.size() >= collectiveLimit) { + deleteEarliestFile(); + } + } + + // If written above the log limit, make a new file + if (limit > 0 && curStream.size() >= limit) { + newFile(); + } + } +} diff --git a/server/core/src/main/java/io/eiren/util/logging/LogManager.java b/server/core/src/main/java/io/eiren/util/logging/LogManager.java index 948e21130d..237dd4a4e3 100644 --- a/server/core/src/main/java/io/eiren/util/logging/LogManager.java +++ b/server/core/src/main/java/io/eiren/util/logging/LogManager.java @@ -3,10 +3,9 @@ import java.io.File; import java.io.IOException; import java.io.PrintStream; -import java.nio.file.Paths; +import java.time.format.DateTimeFormatter; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.ConsoleHandler; -import java.util.logging.FileHandler; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; @@ -30,10 +29,15 @@ public static void initialize(File mainLogDir) if (!mainLogDir.exists()) mainLogDir.mkdirs(); - String lastLogPattern = Paths.get(mainLogDir.getPath(), "log_last_%g.log").toString(); - FileHandler filehandler = new FileHandler(lastLogPattern, 25 * 1000000, 2); - filehandler.setFormatter(loc); - global.addHandler(filehandler); + FileLogHandler fileHandler = new FileLogHandler( + mainLogDir.toPath(), + "slimevr-server", + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"), + 25 * 1000000, + 2 + ); + fileHandler.setFormatter(loc); + global.addHandler(fileHandler); } }