diff --git a/tinylog-impl/src/main/java/org/tinylog/writers/RollingFileWriter.java b/tinylog-impl/src/main/java/org/tinylog/writers/RollingFileWriter.java index 9a9713dfe..dd5da7731 100644 --- a/tinylog-impl/src/main/java/org/tinylog/writers/RollingFileWriter.java +++ b/tinylog-impl/src/main/java/org/tinylog/writers/RollingFileWriter.java @@ -17,6 +17,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -43,6 +44,7 @@ public final class RollingFileWriter extends AbstractFormatPatternWriter { private final int backups; private final boolean buffered; private final boolean writingThread; + private final DynamicPath linkToLatest; private final Charset charset; private ByteArrayWriter writer; @@ -72,8 +74,9 @@ public RollingFileWriter(final Map properties) throws FileNotFou path = new DynamicPath(getFileName(properties)); policies = createPolicies(properties.get("policies")); backups = properties.containsKey("backups") ? Integer.parseInt(properties.get("backups")) : -1; + linkToLatest = properties.containsKey("latest") ? new DynamicPath(properties.get("latest")) : null; - List files = path.getAllFiles(); + List files = removeLinkToLatest(path.getAllFiles()); String fileName; boolean append; @@ -96,7 +99,43 @@ public RollingFileWriter(final Map properties) throws FileNotFou charset = getCharset(properties); buffered = Boolean.parseBoolean(properties.get("buffered")); writingThread = Boolean.parseBoolean(properties.get("writingthread")); - writer = createByteArrayWriter(fileName, append, buffered, false, false); + writer = createByteArrayWriterAndLinkLatest(fileName, append, buffered, false, false); + } + + private List removeLinkToLatest(final List files) { + if (linkToLatest != null) { + String linkToLatestAbsolutePath = linkToLatest.resolve(); + List symlink = new ArrayList<>(); + for (File file : files) { + if (file.getAbsolutePath().equals(linkToLatestAbsolutePath)) { + symlink.add(file); + break; + } + } + files.removeAll(symlink); + } + return files; + } + + private ByteArrayWriter createByteArrayWriterAndLinkLatest(final String fileName, final boolean append, final boolean buffered, + final boolean threadSafe, final boolean shared) throws FileNotFoundException { + ByteArrayWriter writer = AbstractFormatPatternWriter.createByteArrayWriter(fileName, append, buffered, threadSafe, shared); + if (linkToLatest != null) { + String logFile = new File(fileName).getAbsolutePath(); + String linkFile = new File(linkToLatest.resolve()).getAbsolutePath(); + try { + Process process = Runtime.getRuntime().exec(new String[]{"ln", "-sf", logFile, linkFile}); + try { + process.waitFor(); + } catch (InterruptedException intEx) { + InternalLogger.log(Level.WARN, intEx, "Interrupted while creating symlink '" + linkFile + "'"); + } + process.destroy(); + } catch (IOException exception) { + InternalLogger.log(Level.ERROR, exception, "Failed to create symlink '" + linkFile + "'"); + } + } + return writer; } @Override @@ -145,9 +184,11 @@ private void internalWrite(final byte[] data) throws IOException { if (!canBeContinued(data, policies)) { writer.close(); + List existingFiles = removeLinkToLatest(path.getAllFiles()); + deleteBackups(existingFiles, backups); + String fileName = path.resolve(); - deleteBackups(path.getAllFiles(), backups); - writer = createByteArrayWriter(fileName, false, buffered, false, false); + writer = createByteArrayWriterAndLinkLatest(fileName, false, buffered, false, false); for (Policy policy : policies) { policy.reset(); diff --git a/tinylog-impl/src/test/java/org/tinylog/writers/RollingFileWriterTest.java b/tinylog-impl/src/test/java/org/tinylog/writers/RollingFileWriterTest.java index 1319a16f3..bec1c7a5d 100644 --- a/tinylog-impl/src/test/java/org/tinylog/writers/RollingFileWriterTest.java +++ b/tinylog-impl/src/test/java/org/tinylog/writers/RollingFileWriterTest.java @@ -481,4 +481,92 @@ public void isRegistered() throws IOException { assertThat(writer).isInstanceOf(RollingFileWriter.class); } + + /** + * Verifies that symlink to new log segment is created at startup. + * + * @throws IOException + * Failed access to temporary folder or files + */ + @Test + public void createSymlinkToLatestAtStartUp() throws IOException { + File file1 = folder.newFile("0"); + File file2 = folder.newFile("1"); + File file3 = folder.newFile("2"); + File file4 = folder.newFile("3"); + File linkToLatest = folder.newFile("latest"); + + file1.setLastModified(0); + file2.setLastModified(1000); + file3.setLastModified(2000); + file4.setLastModified(3000); + + Map properties = new HashMap<>(); + properties.put("file", new File(folder.getRoot(), "{count}").getAbsolutePath()); + properties.put("latest", linkToLatest.getAbsolutePath()); + properties.put("format", "{message}"); + properties.put("policies", "size: 1MB"); + properties.put("backups", "2"); + + RollingFileWriter writer = new RollingFileWriter(properties); + try { + assertThat(linkToLatest.getCanonicalPath()).isEqualTo(file4.getCanonicalPath()); + assertThat(file1).doesNotExist(); + assertThat(file2).exists(); + assertThat(file3).exists(); + assertThat(file4).exists(); + } finally { + writer.close(); + } + } + + /** + * Verifies that obsolete backup files will be deleted when rolling log file. + * + * @throws IOException + * Failed access to temporary folder or files + */ + @Test + public void updateSymlinkToLatestAtRollOver() throws IOException { + File file1 = folder.newFile("0"); + File file2 = folder.newFile("1"); + File file3 = folder.newFile("2"); + File file4 = folder.newFile("3"); + File file5 = folder.newFile("4"); + File linkToLatest = folder.newFile("latest"); + + file1.setLastModified(0); + file2.setLastModified(1000); + file3.setLastModified(2000); + file4.setLastModified(3000); + file5.delete(); + + Map properties = new HashMap<>(); + properties.put("file", new File(folder.getRoot(), "{count}").getAbsolutePath()); + properties.put("latest", linkToLatest.getAbsolutePath()); + properties.put("format", "{message}"); + properties.put("policies", "size: 10"); + properties.put("backups", "3"); + + RollingFileWriter writer = new RollingFileWriter(properties); + + assertThat(file1).exists(); + assertThat(file2).exists(); + assertThat(file3).exists(); + assertThat(file4).exists(); + assertThat(file5).doesNotExist(); + + writer.write(LogEntryBuilder.empty().message("First").create()); + writer.write(LogEntryBuilder.empty().message("Second").create()); + + assertThat(file1).doesNotExist(); + assertThat(file2).exists(); + assertThat(file3).exists(); + assertThat(file4).hasContent("First" + NEW_LINE); + assertThat(file5).hasContent("Second" + NEW_LINE); + + assertThat(linkToLatest.getCanonicalPath()).isEqualTo(file5.getCanonicalPath()); + + writer.close(); + } }