Fork of Poseidon providing Bukkit #1060 to older Beta versions (b1.0-b1.7.3)

Feature/81 latest log file implementation (#84)

* Created log rotation class

* Added log rotation task

* removed rotation scheduling

* Added log rotation scheduling

* Switched to bukkit task scheduling

* Renamed variable a to logger

* removed un-necessary comments

* moved log rotation scheduling instantiation

Log rotation was instantiated before all startup tasks were complete, missing logs from startup. Instantiation has not been moved AFTER the server prompts "Done (0.059s)! For help, type "help" or "?""

* Rewrite of log rotation logic.

Log rotation logic prior to rewrite moved contents of latest.log into a log with the days date, improperly handling the logs which might contain previous dates e.g. server stopped over midnight.

New logic will handle crashes, and logs containing multiple days dates.

* Make latest log file optional

---------

Co-authored-by: Johny Muffin <jetpackingwolf@gmail.com>

authored by Joshua Reisbord Johny Muffin and committed by GitHub 09cfb203 07612eaa

+194 -14
+19 -6
src/main/java/com/legacyminecraft/poseidon/PoseidonConfig.java
··· 9 9 10 10 public class PoseidonConfig extends Configuration { 11 11 private static PoseidonConfig singleton; 12 - private final int configVersion = 3; 12 + private final int configVersion = 4; 13 13 private Integer[] treeBlacklistIDs; 14 14 15 15 public Integer[] getTreeBlacklistIDs() { ··· 38 38 39 39 private void write() { 40 40 if (this.getString("config-version") == null || Integer.valueOf(this.getString("config-version")) < configVersion) { 41 - System.out.println("Converting to Config Version: " + configVersion); 41 + System.out.println("[Poseidon] Converting from config version " + (this.getString("config-version") == null ? "0" : this.getString("config-version")) + " to " + configVersion); 42 42 convertToNewConfig(); 43 + this.setProperty("config-version", configVersion); 43 44 } 44 45 //Main 45 - generateConfigOption("config-version", 3); 46 + generateConfigOption("config-version", configVersion); 46 47 //Setting 47 48 generateConfigOption("settings.allow-graceful-uuids", true); 48 49 generateConfigOption("settings.delete-duplicate-uuids", false); 49 50 generateConfigOption("settings.save-playerdata-by-uuid", true); 50 - generateConfigOption("settings.per-day-logfile", false); 51 + // Log management and rotation 52 + generateConfigOption("settings.per-day-log-file.info", "This setting causes the server to create a new log file each day. This is useful for log rotation and log file management."); 53 + generateConfigOption("settings.per-day-log-file.enabled", false); 54 + generateConfigOption("settings.per-day-log-file.latest-log.info", "This setting causes the server to create a latest.log similar to modern Minecraft servers. This can be useful for certain control panels and log file management."); 55 + generateConfigOption("settings.per-day-log-file.latest-log.enabled", true); 56 + 51 57 generateConfigOption("settings.fetch-uuids-from", "https://api.mojang.com/profiles/minecraft"); 52 58 generateConfigOption("settings.remove-join-leave-debug", true); 53 59 generateConfigOption("settings.enable-tpc-nodelay", false); ··· 237 243 convertToNewAddress("settings.statistics.enabled", "settings.enable-statistics"); 238 244 convertToNewAddress("settings.allow-graceful-uuids", "allowGracefulUUID"); 239 245 convertToNewAddress("settings.save-playerdata-by-uuid", "savePlayerdataByUUID"); 246 + convertToNewAddress("settings.watchdog.enable", "settings.enable-watchdog"); 247 + // 3-4 Conversion 240 248 241 - convertToNewAddress("settings.enable-watchdog", "settings.watchdog.enable"); 249 + // Don't automatically enable the latest log file for servers that have the per-day-logfile setting enabled as this is a change in behavior 250 + if(this.getString("settings.per-day-logfile") != null && this.getConfigBoolean("settings.per-day-logfile")) { 251 + this.setProperty("settings.per-day-log-file.latest-log.enabled", false); 252 + } 253 + convertToNewAddress("settings.per-day-log-file.enabled", "settings.per-day-logfile"); 242 254 } 243 255 244 256 private boolean convertToNewAddress(String newKey, String oldKey) { ··· 246 258 return false; 247 259 } 248 260 if (this.getString(oldKey) == null) { 261 + System.out.println("[Poseidon] Config: " + oldKey + " does not exist. Skipping conversion."); 249 262 return false; 250 263 } 251 - System.out.println("Converting Config: " + oldKey + " to " + newKey); 264 + System.out.println("[Poseidon] Converting Config: " + oldKey + " to " + newKey); 252 265 Object value = this.getProperty(oldKey); 253 266 this.setProperty(newKey, value); 254 267 this.removeProperty(oldKey);
+150
src/main/java/com/legacyminecraft/poseidon/util/ServerLogRotator.java
··· 1 + package com.legacyminecraft.poseidon.util; 2 + 3 + import org.bukkit.Bukkit; 4 + import com.legacyminecraft.poseidon.PoseidonPlugin; 5 + 6 + import java.io.File; 7 + import java.io.FileWriter; 8 + import java.io.IOException; 9 + import java.io.PrintWriter; 10 + import java.nio.file.Files; 11 + import java.time.Duration; 12 + import java.time.LocalDateTime; 13 + import java.time.ZonedDateTime; 14 + import java.util.Arrays; 15 + import java.util.concurrent.TimeUnit; 16 + import java.util.logging.Logger; 17 + import java.util.logging.*; 18 + 19 + public class ServerLogRotator { 20 + private final String latestLogFileName; 21 + private final Logger logger; 22 + 23 + public ServerLogRotator(String latestLogFileName) { 24 + this.latestLogFileName = latestLogFileName; 25 + this.logger = Logger.getLogger("Minecraft"); 26 + } 27 + 28 + /** 29 + * Checks if the date in the log line is today's date 30 + * @param date The date in the log line. Format: "yyyy-MM-dd" 31 + * @return True if the date in the log line is today's date, false otherwise 32 + */ 33 + private boolean isToday(String date) { 34 + String[] dateParts = date.split("-"); 35 + LocalDateTime logLineDateTime = LocalDateTime.of(Integer.parseInt(dateParts[0]), Integer.parseInt(dateParts[1]), Integer.parseInt(dateParts[2]), 0, 0, 0); 36 + LocalDateTime now = LocalDateTime.now(); 37 + return logLineDateTime.getYear() == now.getYear() && logLineDateTime.getMonthValue() == now.getMonthValue() && logLineDateTime.getDayOfMonth() == now.getDayOfMonth(); 38 + } 39 + 40 + /** 41 + * Archives a log line to a log file with the same date as the date in the log line 42 + * @param parts The log line to archive to a log file haven been split already e.g. ["2024-03-20", "13:02:27", "[INFO]", "This is a log message..."] 43 + */ 44 + private void archiveLine(String[] parts) { 45 + 46 + try { 47 + 48 + String date = parts[0]; 49 + String time = parts[1]; 50 + String logLevel = parts[2]; 51 + String message = String.join(" ", Arrays.copyOfRange(parts, 3, parts.length)); 52 + // check if a log file with this information already exists 53 + File logFile = new File("." + File.separator + "logs" + File.separator + date + ".log"); 54 + if (!logFile.exists()) { 55 + logFile.createNewFile(); 56 + } 57 + // append the log line to the log file with the same date as the date in the log line 58 + FileWriter fileWriter = new FileWriter(logFile, true); 59 + PrintWriter writer = new PrintWriter(fileWriter); 60 + writer.println(date + " " + time + " " + logLevel + " " + message); 61 + writer.close(); 62 + 63 + // catch any exceptions that occur during the process, and log them. IOExceptions are possible when calling createNewFile() 64 + } catch (IOException e) { 65 + logger.log(Level.SEVERE, "[Poseidon] Failed to create new log file!"); 66 + logger.log(Level.SEVERE, e.toString()); 67 + } 68 + } 69 + 70 + /** 71 + * Builds historical logs from the latest.log file. Logs from today's date are kept in the latest.log file, while logs from previous dates are archived to log files with the same date as the date in the log line. 72 + * Note that if latest.log contains logs from multiple days, the logs will be split by date and archived to the appropriate log files. 73 + */ 74 + private void buildHistoricalLogsFromLatestLogFile() { 75 + 76 + logger.log(Level.INFO, "[Poseidon] Building logs from latest.log..."); 77 + 78 + try { 79 + // open latest log file 80 + File latestLog = new File("." + File.separator + "logs" + File.separator + this.latestLogFileName + ".log"); 81 + if (!latestLog.exists()) { 82 + logger.log(Level.INFO, "[Poseidon] No logs to build from latest.log!"); 83 + return; 84 + } 85 + 86 + // split the contents of the latest log file by line (and strip the newline character) 87 + String content = new String(Files.readAllBytes(latestLog.toPath())); 88 + String[] lines = content.split("\n"); 89 + // create a StringBuilder to store today's logs (to write back to latest.log after archiving the rest of the logs) 90 + StringBuilder todayLogs = new StringBuilder(); 91 + 92 + for (String line : lines) { 93 + 94 + String[] splitLine = line.split(" "); 95 + if (splitLine.length < 3) { // all lines will start with a date, time, and log level e.g. "2024-03-20 13:02:27 [INFO]" 96 + continue; 97 + } 98 + 99 + // make sure the first index is a date 100 + if (!splitLine[0].matches("\\d{4}-\\d{2}-\\d{2}")) { 101 + continue; 102 + } 103 + 104 + // if the log line is of today's date, do not archive it (ignore times) 105 + if (isToday(splitLine[0])) { 106 + todayLogs.append(line).append("\n"); 107 + continue; 108 + } 109 + 110 + // archive the log line to a log file with the same date as the date in the log line 111 + archiveLine(splitLine); 112 + 113 + } 114 + 115 + // clear latest.log and write back today's logs from the StringBuilder 116 + FileWriter fileWriter = new FileWriter(latestLog); 117 + PrintWriter writer = new PrintWriter(fileWriter); 118 + writer.print(todayLogs); 119 + writer.close(); 120 + 121 + logger.log(Level.INFO, "[Poseidon] Logs built from latest.log!"); 122 + 123 + // catch any exceptions that occur during the process, and log them 124 + } catch (Exception e) { 125 + logger.log(Level.SEVERE, "[Poseidon] Failed to build logs from latest.log!"); 126 + logger.log(Level.SEVERE, e.toString()); 127 + } 128 + } 129 + 130 + public void start() { 131 + // Calculate the initial delay and period for the log rotation task 132 + ZonedDateTime now = ZonedDateTime.now(); 133 + ZonedDateTime nextRun = now.withHour(0).withMinute(0).withSecond(0).withNano(0); 134 + if(now.compareTo(nextRun) > 0) 135 + nextRun = nextRun.plusDays(1); 136 + Duration duration = Duration.between(now, nextRun); 137 + long initialDelay = duration.getSeconds(); 138 + long period = TimeUnit.DAYS.toSeconds(1); 139 + 140 + // do log rotation immediately upon startup to ensure that logs are archived correctly. 141 + buildHistoricalLogsFromLatestLogFile(); 142 + 143 + // Schedule the log rotation task to run every day at midnight offset by one second to avoid missing logs 144 + logger.log(Level.INFO, "[Poseidon] Log rotation task scheduled for run in " + initialDelay + " seconds, and then every " + period + " seconds."); 145 + logger.log(Level.INFO, "[Poseidon] If latest.log contains logs from earlier, not previously archived dates, they will be archived to the appropriate log files " + 146 + "upon first run of the log rotation task. If log files already exist for these dates, the logs will be appended to the existing log files!"); 147 + Bukkit.getScheduler().scheduleAsyncRepeatingTask(new PoseidonPlugin(), this::buildHistoricalLogsFromLatestLogFile, (initialDelay + 1) * 20, period * 20); 148 + } 149 + } 150 +
+17 -8
src/main/java/net/minecraft/server/ConsoleLogManager.java
··· 41 41 try { 42 42 //Project Poseidon Start 43 43 FileHandler filehandler; 44 - if ((boolean) PoseidonConfig.getInstance().getConfigOption("settings.per-day-logfile")) { 45 - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 46 - String logfile = LocalDate.now().format(formatter); 47 - File log = new File("." + File.separator + "logs" + File.separator); 48 - log.getParentFile().mkdirs(); 49 - log.mkdirs(); 50 - filehandler = new FileHandler("." + File.separator + "logs" + File.separator + logfile + ".log", true); 44 + if ((boolean) PoseidonConfig.getInstance().getConfigOption("settings.per-day-log-file.enabled")) { 45 + //If latest log file is enabled, create a new log file for each day 46 + if ((boolean) PoseidonConfig.getInstance().getConfigOption("settings.per-day-log-file.latest-log.enabled")) { 47 + String latestLogFileName = "latest"; 48 + File log = new File("." + File.separator + "logs" + File.separator); 49 + log.getParentFile().mkdirs(); 50 + log.mkdirs(); 51 + filehandler = new FileHandler("." + File.separator + "logs" + File.separator + latestLogFileName + ".log", true); 52 + } else { 53 + //If latest log file is disabled, create a new log file for each day with the date as the file name 54 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 55 + String logfile = LocalDate.now().format(formatter); 56 + File log = new File("." + File.separator + "logs" + File.separator); 57 + log.getParentFile().mkdirs(); 58 + log.mkdirs(); 59 + filehandler = new FileHandler("." + File.separator + "logs" + File.separator + logfile + ".log", true); 60 + } 51 61 } else { 52 62 // CraftBukkit start 53 63 String pattern = (String) server.options.valueOf("log-pattern"); ··· 58 68 // CraftBukkit start 59 69 } 60 70 //Project Poseidon End 61 - 62 71 63 72 filehandler.setFormatter(consolelogformatter); 64 73 a.addHandler(filehandler);
+8
src/main/java/net/minecraft/server/MinecraftServer.java
··· 1 1 package net.minecraft.server; 2 2 3 3 import com.legacyminecraft.poseidon.PoseidonConfig; 4 + import com.legacyminecraft.poseidon.util.ServerLogRotator; 4 5 import com.projectposeidon.johnymuffin.UUIDManager; 5 6 import com.legacyminecraft.poseidon.watchdog.WatchDogThread; 6 7 import jline.ConsoleReader; ··· 196 197 long elapsed = System.nanoTime() - j; 197 198 String time = String.format("%.3fs", elapsed / 10000000000.0D); 198 199 log.info("Done (" + time + ")! For help, type \"help\" or \"?\""); 200 + 201 + // log rotator process start. 202 + if ((boolean) PoseidonConfig.getInstance().getConfigOption("settings.per-day-log-file.enabled") && (boolean) PoseidonConfig.getInstance().getConfigOption("settings.per-day-log-file.latest-log.enabled")) { 203 + String latestLogFileName = "latest"; 204 + ServerLogRotator serverLogRotator = new ServerLogRotator(latestLogFileName); 205 + serverLogRotator.start(); 206 + } 199 207 200 208 if (this.propertyManager.properties.containsKey("spawn-protection")) { 201 209 log.info("'spawn-protection' in server.properties has been moved to 'settings.spawn-radius' in bukkit.yml. I will move your config for you.");