Skip to content

Commit

Permalink
Feature/81 latest log file implementation (#84)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
joshuareisbord and RhysB authored Mar 30, 2024
1 parent 07612ea commit 09cfb20
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 14 deletions.
25 changes: 19 additions & 6 deletions src/main/java/com/legacyminecraft/poseidon/PoseidonConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

public class PoseidonConfig extends Configuration {
private static PoseidonConfig singleton;
private final int configVersion = 3;
private final int configVersion = 4;
private Integer[] treeBlacklistIDs;

public Integer[] getTreeBlacklistIDs() {
Expand Down Expand Up @@ -38,16 +38,22 @@ public void resetConfig() {

private void write() {
if (this.getString("config-version") == null || Integer.valueOf(this.getString("config-version")) < configVersion) {
System.out.println("Converting to Config Version: " + configVersion);
System.out.println("[Poseidon] Converting from config version " + (this.getString("config-version") == null ? "0" : this.getString("config-version")) + " to " + configVersion);
convertToNewConfig();
this.setProperty("config-version", configVersion);
}
//Main
generateConfigOption("config-version", 3);
generateConfigOption("config-version", configVersion);
//Setting
generateConfigOption("settings.allow-graceful-uuids", true);
generateConfigOption("settings.delete-duplicate-uuids", false);
generateConfigOption("settings.save-playerdata-by-uuid", true);
generateConfigOption("settings.per-day-logfile", false);
// Log management and rotation
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.");
generateConfigOption("settings.per-day-log-file.enabled", false);
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.");
generateConfigOption("settings.per-day-log-file.latest-log.enabled", true);

generateConfigOption("settings.fetch-uuids-from", "https://api.mojang.com/profiles/minecraft");
generateConfigOption("settings.remove-join-leave-debug", true);
generateConfigOption("settings.enable-tpc-nodelay", false);
Expand Down Expand Up @@ -237,18 +243,25 @@ private void convertToNewConfig() {
convertToNewAddress("settings.statistics.enabled", "settings.enable-statistics");
convertToNewAddress("settings.allow-graceful-uuids", "allowGracefulUUID");
convertToNewAddress("settings.save-playerdata-by-uuid", "savePlayerdataByUUID");
convertToNewAddress("settings.watchdog.enable", "settings.enable-watchdog");
// 3-4 Conversion

convertToNewAddress("settings.enable-watchdog", "settings.watchdog.enable");
// 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
if(this.getString("settings.per-day-logfile") != null && this.getConfigBoolean("settings.per-day-logfile")) {
this.setProperty("settings.per-day-log-file.latest-log.enabled", false);
}
convertToNewAddress("settings.per-day-log-file.enabled", "settings.per-day-logfile");
}

private boolean convertToNewAddress(String newKey, String oldKey) {
if (this.getString(newKey) != null) {
return false;
}
if (this.getString(oldKey) == null) {
System.out.println("[Poseidon] Config: " + oldKey + " does not exist. Skipping conversion.");
return false;
}
System.out.println("Converting Config: " + oldKey + " to " + newKey);
System.out.println("[Poseidon] Converting Config: " + oldKey + " to " + newKey);
Object value = this.getProperty(oldKey);
this.setProperty(newKey, value);
this.removeProperty(oldKey);
Expand Down
150 changes: 150 additions & 0 deletions src/main/java/com/legacyminecraft/poseidon/util/ServerLogRotator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package com.legacyminecraft.poseidon.util;

import org.bukkit.Bukkit;
import com.legacyminecraft.poseidon.PoseidonPlugin;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.logging.*;

public class ServerLogRotator {
private final String latestLogFileName;
private final Logger logger;

public ServerLogRotator(String latestLogFileName) {
this.latestLogFileName = latestLogFileName;
this.logger = Logger.getLogger("Minecraft");
}

/**
* Checks if the date in the log line is today's date
* @param date The date in the log line. Format: "yyyy-MM-dd"
* @return True if the date in the log line is today's date, false otherwise
*/
private boolean isToday(String date) {
String[] dateParts = date.split("-");
LocalDateTime logLineDateTime = LocalDateTime.of(Integer.parseInt(dateParts[0]), Integer.parseInt(dateParts[1]), Integer.parseInt(dateParts[2]), 0, 0, 0);
LocalDateTime now = LocalDateTime.now();
return logLineDateTime.getYear() == now.getYear() && logLineDateTime.getMonthValue() == now.getMonthValue() && logLineDateTime.getDayOfMonth() == now.getDayOfMonth();
}

/**
* Archives a log line to a log file with the same date as the date in the log line
* @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..."]
*/
private void archiveLine(String[] parts) {

try {

String date = parts[0];
String time = parts[1];
String logLevel = parts[2];
String message = String.join(" ", Arrays.copyOfRange(parts, 3, parts.length));
// check if a log file with this information already exists
File logFile = new File("." + File.separator + "logs" + File.separator + date + ".log");
if (!logFile.exists()) {
logFile.createNewFile();
}
// append the log line to the log file with the same date as the date in the log line
FileWriter fileWriter = new FileWriter(logFile, true);
PrintWriter writer = new PrintWriter(fileWriter);
writer.println(date + " " + time + " " + logLevel + " " + message);
writer.close();

// catch any exceptions that occur during the process, and log them. IOExceptions are possible when calling createNewFile()
} catch (IOException e) {
logger.log(Level.SEVERE, "[Poseidon] Failed to create new log file!");
logger.log(Level.SEVERE, e.toString());
}
}

/**
* 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.
* Note that if latest.log contains logs from multiple days, the logs will be split by date and archived to the appropriate log files.
*/
private void buildHistoricalLogsFromLatestLogFile() {

logger.log(Level.INFO, "[Poseidon] Building logs from latest.log...");

try {
// open latest log file
File latestLog = new File("." + File.separator + "logs" + File.separator + this.latestLogFileName + ".log");
if (!latestLog.exists()) {
logger.log(Level.INFO, "[Poseidon] No logs to build from latest.log!");
return;
}

// split the contents of the latest log file by line (and strip the newline character)
String content = new String(Files.readAllBytes(latestLog.toPath()));
String[] lines = content.split("\n");
// create a StringBuilder to store today's logs (to write back to latest.log after archiving the rest of the logs)
StringBuilder todayLogs = new StringBuilder();

for (String line : lines) {

String[] splitLine = line.split(" ");
if (splitLine.length < 3) { // all lines will start with a date, time, and log level e.g. "2024-03-20 13:02:27 [INFO]"
continue;
}

// make sure the first index is a date
if (!splitLine[0].matches("\\d{4}-\\d{2}-\\d{2}")) {
continue;
}

// if the log line is of today's date, do not archive it (ignore times)
if (isToday(splitLine[0])) {
todayLogs.append(line).append("\n");
continue;
}

// archive the log line to a log file with the same date as the date in the log line
archiveLine(splitLine);

}

// clear latest.log and write back today's logs from the StringBuilder
FileWriter fileWriter = new FileWriter(latestLog);
PrintWriter writer = new PrintWriter(fileWriter);
writer.print(todayLogs);
writer.close();

logger.log(Level.INFO, "[Poseidon] Logs built from latest.log!");

// catch any exceptions that occur during the process, and log them
} catch (Exception e) {
logger.log(Level.SEVERE, "[Poseidon] Failed to build logs from latest.log!");
logger.log(Level.SEVERE, e.toString());
}
}

public void start() {
// Calculate the initial delay and period for the log rotation task
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime nextRun = now.withHour(0).withMinute(0).withSecond(0).withNano(0);
if(now.compareTo(nextRun) > 0)
nextRun = nextRun.plusDays(1);
Duration duration = Duration.between(now, nextRun);
long initialDelay = duration.getSeconds();
long period = TimeUnit.DAYS.toSeconds(1);

// do log rotation immediately upon startup to ensure that logs are archived correctly.
buildHistoricalLogsFromLatestLogFile();

// Schedule the log rotation task to run every day at midnight offset by one second to avoid missing logs
logger.log(Level.INFO, "[Poseidon] Log rotation task scheduled for run in " + initialDelay + " seconds, and then every " + period + " seconds.");
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 " +
"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!");
Bukkit.getScheduler().scheduleAsyncRepeatingTask(new PoseidonPlugin(), this::buildHistoricalLogsFromLatestLogFile, (initialDelay + 1) * 20, period * 20);
}
}

25 changes: 17 additions & 8 deletions src/main/java/net/minecraft/server/ConsoleLogManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,23 @@ public static void init(MinecraftServer server) {
try {
//Project Poseidon Start
FileHandler filehandler;
if ((boolean) PoseidonConfig.getInstance().getConfigOption("settings.per-day-logfile")) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String logfile = LocalDate.now().format(formatter);
File log = new File("." + File.separator + "logs" + File.separator);
log.getParentFile().mkdirs();
log.mkdirs();
filehandler = new FileHandler("." + File.separator + "logs" + File.separator + logfile + ".log", true);
if ((boolean) PoseidonConfig.getInstance().getConfigOption("settings.per-day-log-file.enabled")) {
//If latest log file is enabled, create a new log file for each day
if ((boolean) PoseidonConfig.getInstance().getConfigOption("settings.per-day-log-file.latest-log.enabled")) {
String latestLogFileName = "latest";
File log = new File("." + File.separator + "logs" + File.separator);
log.getParentFile().mkdirs();
log.mkdirs();
filehandler = new FileHandler("." + File.separator + "logs" + File.separator + latestLogFileName + ".log", true);
} else {
//If latest log file is disabled, create a new log file for each day with the date as the file name
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String logfile = LocalDate.now().format(formatter);
File log = new File("." + File.separator + "logs" + File.separator);
log.getParentFile().mkdirs();
log.mkdirs();
filehandler = new FileHandler("." + File.separator + "logs" + File.separator + logfile + ".log", true);
}
} else {
// CraftBukkit start
String pattern = (String) server.options.valueOf("log-pattern");
Expand All @@ -59,7 +69,6 @@ public static void init(MinecraftServer server) {
}
//Project Poseidon End


filehandler.setFormatter(consolelogformatter);
a.addHandler(filehandler);
global.addHandler(filehandler); // CraftBukkit
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/net/minecraft/server/MinecraftServer.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.minecraft.server;

import com.legacyminecraft.poseidon.PoseidonConfig;
import com.legacyminecraft.poseidon.util.ServerLogRotator;
import com.projectposeidon.johnymuffin.UUIDManager;
import com.legacyminecraft.poseidon.watchdog.WatchDogThread;
import jline.ConsoleReader;
Expand Down Expand Up @@ -197,6 +198,13 @@ private boolean init() throws UnknownHostException { // CraftBukkit - added thro
String time = String.format("%.3fs", elapsed / 10000000000.0D);
log.info("Done (" + time + ")! For help, type \"help\" or \"?\"");

// log rotator process start.
if ((boolean) PoseidonConfig.getInstance().getConfigOption("settings.per-day-log-file.enabled") && (boolean) PoseidonConfig.getInstance().getConfigOption("settings.per-day-log-file.latest-log.enabled")) {
String latestLogFileName = "latest";
ServerLogRotator serverLogRotator = new ServerLogRotator(latestLogFileName);
serverLogRotator.start();
}

if (this.propertyManager.properties.containsKey("spawn-protection")) {
log.info("'spawn-protection' in server.properties has been moved to 'settings.spawn-radius' in bukkit.yml. I will move your config for you.");
this.server.setSpawnRadius(this.propertyManager.getInt("spawn-protection", 16));
Expand Down

0 comments on commit 09cfb20

Please sign in to comment.