Skip to content

Commit 8b07a0c

Browse files
authored
Add rolling scheme via maxBackupCount. (#14)
1 parent d22a005 commit 8b07a0c

File tree

6 files changed

+365
-42
lines changed

6 files changed

+365
-42
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
### (2021-07-29) v0.9.3
2+
3+
- Switched from `File#renameTo(File)` to the more robust
4+
`Files.move(Path, Path, CopyOptions...)` alternative. (#14)
5+
6+
- Add rolling support via `maxBackupCount`. (#14)
7+
8+
- Stop policies after stream close. (#26)
9+
110
### (2020-01-10) v0.9.2
211

312
- Shutdown the default `ScheduledExecutorService` at JVM exit. (#12)

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ RotationConfig config = RotationConfig
3232
.file("/tmp/app.log")
3333
.filePattern("/tmp/app-%d{yyyyMMdd-HHmmss.SSS}.log")
3434
.policy(new SizeBasedRotationPolicy(1024 * 1024 * 100 /* 100MiB */))
35+
.compress(true)
3536
.policy(DailyRotationPolicy.getInstance())
3637
.build();
3738

@@ -40,16 +41,35 @@ try (RotatingFileOutputStream stream = new RotatingFileOutputStream(config)) {
4041
}
4142
```
4243

44+
Using `maxBackupCount`, one can also introduce a rolling scheme where rotated
45+
files will be named as `file.0`, `file.1`, `file.2`, ..., `file.N` in the order
46+
from the newest to the oldest, `N` denoting the `maxBackupCount`:
47+
48+
```java
49+
RotationConfig config = RotationConfig
50+
.builder()
51+
.file("/tmp/app.log")
52+
.maxBackupCount(10) // Set `filePattern` to `file.%i` and keep
53+
// the most recent 10 files.
54+
.policy(new SizeBasedRotationPolicy(1024 * 1024 * 100 /* 100MiB */))
55+
.build();
56+
57+
try (RotatingFileOutputStream stream = new RotatingFileOutputStream(config)) {
58+
stream.write("Hello, world!".getBytes(StandardCharsets.UTF_8));
59+
}
60+
```
61+
4362
`RotationConfig.Builder` supports the following methods:
4463

4564
| Method(s) | Description |
4665
| --------- | ----------- |
4766
| `file(File)`<br/>`file(String)` | file accessed (e.g., `/tmp/app.log`) |
48-
| `filePattern(RotatingFilePattern)`<br/>`filePattern(String)`| rotating file pattern (e.g., `/tmp/app-%d{yyyyMMdd-HHmmss-SSS}.log`) |
67+
| `filePattern(RotatingFilePattern)`<br/>`filePattern(String)`| The pattern used to generate files for moving after rotation, e.g., `/tmp/app-%d{yyyyMMdd-HHmmss-SSS}.log`. This option cannot be combined with `maxBackupCount`. |
4968
| `policy(RotationPolicy)`<br/>`policies(Set<RotationPolicy> policies)` | rotation policies |
69+
| `maxBackupCount(int)` | If greater than zero, rotated files will be named as `file.0`, `file.1`, `file.2`, ..., `file.N` in the order from the newest to the oldest, where `N` denoting the `maxBackupCount`. `maxBackupCount` defaults to `-1`, that is, no rolling. This option cannot be combined with `filePattern` or `compress`. |
5070
| `executorService(ScheduledExecutorService)` | scheduler for time-based policies and compression tasks |
5171
| `append(boolean)` | append while opening the `file` (defaults to `true`) |
52-
| `compress(boolean)` | GZIP compression after rotation (defaults to `false`) |
72+
| `compress(boolean)` | Toggles GZIP compression after rotation and defaults to `false`. This option cannot be combined with `maxBackupCount`. |
5373
| `clock(Clock)` | clock for retrieving date and time (defaults to `SystemClock`) |
5474
| `callback(RotationCallback)`<br/>`callbacks(Set<RotationCallback>)` | rotation callbacks (defaults to `LoggingRotationCallback`) |
5575

@@ -136,6 +156,7 @@ methods.
136156
- [Jonas (yawkat) Konrad](https://yawk.at/) (`RotatingFileOutputStream`
137157
thread-safety improvements)
138158
- [Lukas Bradley](https://github.com/lukasbradley/)
159+
- [Liran Mendelovich](https://github.com/liran2000/) (rolling via `maxBackupCount`)
139160

140161
# License
141162

src/main/java/com/vlkan/rfos/RotatingFileOutputStream.java

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,20 @@
2020
import org.slf4j.Logger;
2121
import org.slf4j.LoggerFactory;
2222

23-
import java.io.*;
23+
import java.io.File;
24+
import java.io.FileInputStream;
25+
import java.io.FileOutputStream;
26+
import java.io.IOException;
27+
import java.io.InputStream;
28+
import java.io.OutputStream;
29+
import java.nio.file.Files;
30+
import java.nio.file.Paths;
31+
import java.nio.file.StandardCopyOption;
2432
import java.time.Instant;
25-
import java.util.*;
33+
import java.util.ArrayList;
34+
import java.util.List;
35+
import java.util.Objects;
36+
import java.util.Set;
2637
import java.util.function.Consumer;
2738
import java.util.zip.GZIPOutputStream;
2839

@@ -106,15 +117,18 @@ private synchronized void unsafeRotate(RotationPolicy policy, Instant instant) t
106117
invokeCallbacks(callback -> callback.onClose(policy, instant, stream));
107118
stream.close();
108119

109-
// Rename the file.
110-
File rotatedFile = config.getFilePattern().create(instant).getAbsoluteFile();
111-
LOGGER.debug("renaming {file={}, rotatedFile={}}", config.getFile(), rotatedFile);
112-
boolean renamed = config.getFile().renameTo(rotatedFile);
113-
if (!renamed) {
114-
String message = String.format("rename failure {file=%s, rotatedFile=%s}", config.getFile(), rotatedFile);
115-
IOException error = new IOException(message);
116-
invokeCallbacks(callback -> callback.onFailure(policy, instant, rotatedFile, error));
117-
return;
120+
// Backup file, if enabled.
121+
File rotatedFile;
122+
if (config.getMaxBackupCount() > 0) {
123+
renameBackups();
124+
rotatedFile = backupFile();
125+
}
126+
127+
// Otherwise, rename using the provided file pattern.
128+
else {
129+
rotatedFile = config.getFilePattern().create(instant).getAbsoluteFile();
130+
LOGGER.debug("renaming {file={}, rotatedFile={}}", config.getFile(), rotatedFile);
131+
renameFile(config.getFile(), rotatedFile);
118132
}
119133

120134
// Re-open the file.
@@ -132,6 +146,45 @@ private synchronized void unsafeRotate(RotationPolicy policy, Instant instant) t
132146

133147
}
134148

149+
private void renameBackups() throws IOException {
150+
File dstFile = getBackupFile(config.getMaxBackupCount() - 1);
151+
for (int backupIndex = config.getMaxBackupCount() - 2; backupIndex >= 0; backupIndex--) {
152+
File srcFile = getBackupFile(backupIndex);
153+
if (!srcFile.exists()) {
154+
continue;
155+
}
156+
LOGGER.debug("renaming backup {srcFile={}, dstFile={}}", srcFile, dstFile);
157+
renameFile(srcFile, dstFile);
158+
dstFile = srcFile;
159+
}
160+
}
161+
162+
private File backupFile() throws IOException {
163+
File dstFile = getBackupFile(0);
164+
File srcFile = config.getFile();
165+
LOGGER.debug("renaming for backup {srcFile={}, dstFile={}}", srcFile, dstFile);
166+
renameFile(srcFile, dstFile);
167+
return dstFile;
168+
}
169+
170+
private static void renameFile(File srcFile, File dstFile) throws IOException {
171+
Files.move(
172+
srcFile.toPath(),
173+
dstFile.toPath(),
174+
StandardCopyOption.REPLACE_EXISTING/*, // The rest of the arguments (atomic & copy-attr) are pretty
175+
StandardCopyOption.ATOMIC_MOVE, // much platform-dependent and JVM throws an "unsupported
176+
StandardCopyOption.COPY_ATTRIBUTES*/); // option" exception at runtime.
177+
}
178+
179+
private File getBackupFile(int backupIndex) {
180+
String parent = config.getFile().getParent();
181+
if (parent == null) {
182+
parent = ".";
183+
}
184+
String fileName = config.getFile().getName() + '.' + backupIndex;
185+
return Paths.get(parent, fileName).toFile();
186+
}
187+
135188
private void asyncCompress(RotationPolicy policy, Instant instant, File rotatedFile) {
136189
config.getExecutorService().execute(new Runnable() {
137190

src/main/java/com/vlkan/rfos/RotatingFilePattern.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424

2525
public class RotatingFilePattern {
2626

27+
private static final Locale DEFAULT_LOCALE = Locale.getDefault();
28+
29+
private static final ZoneId DEFAULT_TIME_ZONE_ID = TimeZone.getDefault().toZoneId();
30+
2731
private static final char ESCAPE_CHAR = '%';
2832

2933
private static final char DATE_TIME_DIRECTIVE_CHAR = 'd';
@@ -190,10 +194,18 @@ public String getPattern() {
190194
return pattern;
191195
}
192196

197+
public static Locale getDefaultLocale() {
198+
return DEFAULT_LOCALE;
199+
}
200+
193201
public Locale getLocale() {
194202
return locale;
195203
}
196204

205+
public static ZoneId getDefaultTimeZoneId() {
206+
return DEFAULT_TIME_ZONE_ID;
207+
}
208+
197209
public ZoneId getTimeZoneId() {
198210
return timeZoneId;
199211
}
@@ -226,24 +238,24 @@ public static final class Builder {
226238

227239
private String pattern;
228240

229-
private Locale locale = Locale.getDefault();
241+
private Locale locale = DEFAULT_LOCALE;
230242

231-
private ZoneId timeZoneId = TimeZone.getDefault().toZoneId();
243+
private ZoneId timeZoneId = DEFAULT_TIME_ZONE_ID;
232244

233245
private Builder() {}
234246

235247
public Builder pattern(String pattern) {
236-
this.pattern = pattern;
248+
this.pattern = Objects.requireNonNull(pattern, "pattern");
237249
return this;
238250
}
239251

240252
public Builder locale(Locale locale) {
241-
this.locale = locale;
253+
this.locale = Objects.requireNonNull(locale, "locale");
242254
return this;
243255
}
244256

245257
public Builder timeZoneId(ZoneId timeZoneId) {
246-
this.timeZoneId = timeZoneId;
258+
this.timeZoneId = Objects.requireNonNull(timeZoneId, "timeZoneId");
247259
return this;
248260
}
249261

@@ -254,8 +266,6 @@ public RotatingFilePattern build() {
254266

255267
private void validate() {
256268
Objects.requireNonNull(pattern, "file");
257-
Objects.requireNonNull(locale, "locale");
258-
Objects.requireNonNull(timeZoneId, "timeZoneId");
259269
}
260270

261271
}

0 commit comments

Comments
 (0)