diff --git a/photon-server/src/main/java/org/photonvision/common/configuration/ConfigManager.java b/photon-server/src/main/java/org/photonvision/common/configuration/ConfigManager.java index 00b2d6c62..d6323d351 100644 --- a/photon-server/src/main/java/org/photonvision/common/configuration/ConfigManager.java +++ b/photon-server/src/main/java/org/photonvision/common/configuration/ConfigManager.java @@ -22,8 +22,12 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; import java.util.*; import java.util.stream.Collectors; import org.photonvision.common.logging.LogGroup; @@ -353,11 +357,28 @@ public class ConfigManager { requestSave(); } + public Path getLogsDir() { + return Path.of(configDirectoryFile.toString(), "logs"); + } + + public static final String LOG_PREFIX = "photonvision-"; + public static final String LOG_EXT = ".log"; + public static final String LOG_DATE_TIME_FORMAT = "yyyy-M-d_hh-mm-ss"; + + public String taToLogFname(TemporalAccessor date) { + var dateString = DateTimeFormatter.ofPattern(LOG_DATE_TIME_FORMAT).format(date); + return LOG_PREFIX + dateString + LOG_EXT; + } + + public Date logFnameToDate(String fname) throws ParseException { + // Strip away known unneded portions of the log file name + fname = fname.replace(LOG_PREFIX, "").replace(LOG_EXT, ""); + DateFormat format = new SimpleDateFormat(LOG_DATE_TIME_FORMAT); + return format.parse(fname); + } + public Path getLogPath() { - var dateString = DateTimeFormatter.ofPattern("yyyy-M-d_hh-mm-ss").format(LocalDateTime.now()); - var logFile = - Path.of(configDirectoryFile.toString(), "logs", "photonvision-" + dateString + ".log") - .toFile(); + var logFile = Path.of(this.getLogsDir().toString(), taToLogFname(LocalDateTime.now())).toFile(); if (!logFile.getParentFile().exists()) logFile.getParentFile().mkdirs(); return logFile.toPath(); } diff --git a/photon-server/src/main/java/org/photonvision/common/logging/Logger.java b/photon-server/src/main/java/org/photonvision/common/logging/Logger.java index 2b1ba21a0..6d3b64786 100644 --- a/photon-server/src/main/java/org/photonvision/common/logging/Logger.java +++ b/photon-server/src/main/java/org/photonvision/common/logging/Logger.java @@ -19,10 +19,13 @@ package org.photonvision.common.logging; import java.io.*; import java.nio.file.Path; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.function.Supplier; import org.apache.commons.lang3.tuple.Pair; @@ -45,6 +48,8 @@ public class Logger { public static final String ANSI_CYAN = "\u001B[36m"; public static final String ANSI_WHITE = "\u001B[37m"; + public static final int MAX_LOGS_TO_KEEP = 100; + private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @@ -105,6 +110,7 @@ public class Logger { currentAppenders.add(new ConsoleLogAppender()); currentAppenders.add(uiLogAppender); addFileAppender(ConfigManager.getInstance().getLogPath()); + cleanLogs(ConfigManager.getInstance().getLogsDir()); } @SuppressWarnings("ResultOfMethodCallIgnored") @@ -121,6 +127,48 @@ public class Logger { currentAppenders.add(new FileLogAppender(logFilePath)); } + public static void cleanLogs(Path folderToClean) { + + LinkedList logFileList = + new LinkedList<>(Arrays.asList(folderToClean.toFile().listFiles())); + HashMap logFileStartDateMap = new HashMap<>(); + + // Remove any files from the list for which we can't parse a start date from their name. + // Simultaneously populate our HashMap with Date objects repeseting the file-name + // indicated log start time. + logFileList.removeIf( + (File arg0) -> { + try { + logFileStartDateMap.put( + arg0, ConfigManager.getInstance().logFnameToDate(arg0.getName())); + return false; + } catch (ParseException e) { + return true; + } + }); + + // Execute a sort on the log file list by date in the filename. + logFileList.sort( + (File arg0, File arg1) -> { + Date date0 = logFileStartDateMap.get(arg0); + Date date1 = logFileStartDateMap.get(arg1); + return date1.compareTo(date0); + }); + + int logCounter = 0; + for (File file : logFileList) { + // Due to filtering above, everything in logFileList should be a log file + if (logCounter < MAX_LOGS_TO_KEEP) { + // Skip over the first MAX_LOGS_TO_KEEP files + logCounter++; + continue; + } else { + // Delete this file. + file.delete(); + } + } + } + public static void setLevel(LogGroup group, LogLevel newLevel) { levelMap.put(group, newLevel); } diff --git a/photon-server/src/test/java/org/photonvision/common/util/LogFileManagementTest.java b/photon-server/src/test/java/org/photonvision/common/util/LogFileManagementTest.java new file mode 100644 index 000000000..866ca2d62 --- /dev/null +++ b/photon-server/src/test/java/org/photonvision/common/util/LogFileManagementTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2020 Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision.common.util; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.WildcardFileFilter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.photonvision.common.configuration.ConfigManager; +import org.photonvision.common.logging.Logger; + +public class LogFileManagementTest { + + @Test + public void fileCleanupTest() throws IOException { + + // Ensure we instantiate the new log correctly + ConfigManager.getInstance(); + + String testDir = ConfigManager.getInstance().getLogsDir().toString() + "/test"; + + Files.createDirectories(Path.of(testDir)); + + // Create a bunch of log files with dummy contents. + for (int fileIdx = 0; fileIdx < Logger.MAX_LOGS_TO_KEEP + 5; fileIdx++) { + String fname = + ConfigManager.getInstance() + .taToLogFname( + LocalDateTime.ofEpochSecond(1500000000 + fileIdx * 60, 0, ZoneOffset.UTC)); + try { + FileWriter testLogWriter = new FileWriter(Path.of(testDir, fname).toString()); + testLogWriter.write("Test log contents created for testing purposes only"); + testLogWriter.close(); + } catch (IOException e) { + Assertions.fail("Could not create test files"); + } + } + + // Confirm new log files were created + Assertions.assertEquals( + true, + Logger.MAX_LOGS_TO_KEEP + 5 <= countLogFiles(testDir), + "Not enough log files discovered"); + + // Run the log cleanup routine + Logger.cleanLogs(Path.of(testDir)); + + // Confirm we deleted log files + Assertions.assertEquals( + true, Logger.MAX_LOGS_TO_KEEP == countLogFiles(testDir), "Not enough log files deleted"); + + // Clean uptest directory + org.photonvision.common.util.file.FileUtils.deleteDirectory(Path.of(testDir)); + Files.delete(Path.of(testDir)); + } + + private int countLogFiles(String testDir) { + return FileUtils.listFiles( + new File(testDir), new WildcardFileFilter("photonvision-*.log"), null) + .size(); + } +}