1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.logging.log4j.core.appender.rolling;
18
19 import java.io.File;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collections;
23 import java.util.List;
24 import java.util.Objects;
25 import java.util.concurrent.TimeUnit;
26 import java.util.zip.Deflater;
27
28 import org.apache.logging.log4j.Logger;
29 import org.apache.logging.log4j.core.appender.rolling.action.Action;
30 import org.apache.logging.log4j.core.appender.rolling.action.CommonsCompressAction;
31 import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction;
32 import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
33 import org.apache.logging.log4j.core.appender.rolling.action.GzCompressAction;
34 import org.apache.logging.log4j.core.appender.rolling.action.ZipCompressAction;
35 import org.apache.logging.log4j.core.config.Configuration;
36 import org.apache.logging.log4j.core.config.plugins.Plugin;
37 import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
38 import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
39 import org.apache.logging.log4j.core.config.plugins.PluginElement;
40 import org.apache.logging.log4j.core.config.plugins.PluginFactory;
41 import org.apache.logging.log4j.core.lookup.StrSubstitutor;
42 import org.apache.logging.log4j.core.util.Integers;
43 import org.apache.logging.log4j.status.StatusLogger;
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78 @Plugin(name = "DefaultRolloverStrategy", category = "Core", printObject = true)
79 public class DefaultRolloverStrategy implements RolloverStrategy {
80
81
82
83
84
85
86 static enum FileExtensions {
87 ZIP(".zip") {
88 @Override
89 Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
90 final int compressionLevel) {
91 return new ZipCompressAction(source(renameTo), target(compressedName), deleteSource, compressionLevel);
92 }
93 },
94 GZ(".gz") {
95 @Override
96 Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
97 final int compressionLevel) {
98 return new GzCompressAction(source(renameTo), target(compressedName), deleteSource);
99 }
100 },
101 BZIP2(".bz2") {
102 @Override
103 Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
104 final int compressionLevel) {
105
106 return new CommonsCompressAction("bzip2", source(renameTo), target(compressedName), deleteSource);
107 }
108 },
109 DEFLATE(".deflate") {
110 @Override
111 Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
112 final int compressionLevel) {
113
114 return new CommonsCompressAction("deflate", source(renameTo), target(compressedName), deleteSource);
115 }
116 },
117 PACK200(".pack200") {
118 @Override
119 Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
120 final int compressionLevel) {
121
122 return new CommonsCompressAction("pack200", source(renameTo), target(compressedName), deleteSource);
123 }
124 },
125 XZ(".xz") {
126 @Override
127 Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
128 final int compressionLevel) {
129
130 return new CommonsCompressAction("xz", source(renameTo), target(compressedName), deleteSource);
131 }
132 };
133
134 static FileExtensions lookup(final String fileExtension) {
135 for (final FileExtensions ext : values()) {
136 if (ext.isExtensionFor(fileExtension)) {
137 return ext;
138 }
139 }
140 return null;
141 }
142
143 private final String extension;
144
145 private FileExtensions(final String extension) {
146 Objects.requireNonNull(extension, "extension");
147 this.extension = extension;
148 }
149
150 abstract Action createCompressAction(String renameTo, String compressedName, boolean deleteSource,
151 int compressionLevel);
152
153 String getExtension() {
154 return extension;
155 }
156
157 boolean isExtensionFor(final String s) {
158 return s.endsWith(this.extension);
159 }
160
161 int length() {
162 return extension.length();
163 }
164
165 File source(final String fileName) {
166 return new File(fileName);
167 }
168
169 File target(final String fileName) {
170 return new File(fileName);
171 }
172 };
173
174
175
176
177 protected static final Logger LOGGER = StatusLogger.getLogger();
178
179 private static final int MIN_WINDOW_SIZE = 1;
180 private static final int DEFAULT_WINDOW_SIZE = 7;
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195 @PluginFactory
196 public static DefaultRolloverStrategy createStrategy(
197
198 @PluginAttribute("max") final String max,
199 @PluginAttribute("min") final String min,
200 @PluginAttribute("fileIndex") final String fileIndex,
201 @PluginAttribute("compressionLevel") final String compressionLevelStr,
202 @PluginElement("Actions") final Action[] customActions,
203 @PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true)
204 final boolean stopCustomActionsOnError,
205 @PluginConfiguration final Configuration config) {
206
207 final boolean useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max");
208 int minIndex = MIN_WINDOW_SIZE;
209 if (min != null) {
210 minIndex = Integer.parseInt(min);
211 if (minIndex < 1) {
212 LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
213 minIndex = MIN_WINDOW_SIZE;
214 }
215 }
216 int maxIndex = DEFAULT_WINDOW_SIZE;
217 if (max != null) {
218 maxIndex = Integer.parseInt(max);
219 if (maxIndex < minIndex) {
220 maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
221 LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
222 }
223 }
224 final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
225 return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, config.getStrSubstitutor(),
226 customActions, stopCustomActionsOnError);
227 }
228
229
230
231
232 private final int maxIndex;
233
234
235
236
237 private final int minIndex;
238 private final boolean useMax;
239 private final StrSubstitutor strSubstitutor;
240 private final int compressionLevel;
241 private final List<Action> customActions;
242 private final boolean stopCustomActionsOnError;
243
244
245
246
247
248
249
250
251
252 protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
253 final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions,
254 final boolean stopCustomActionsOnError) {
255 this.minIndex = minIndex;
256 this.maxIndex = maxIndex;
257 this.useMax = useMax;
258 this.compressionLevel = compressionLevel;
259 this.strSubstitutor = strSubstitutor;
260 this.stopCustomActionsOnError = stopCustomActionsOnError;
261 this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions);
262 }
263
264 public int getCompressionLevel() {
265 return this.compressionLevel;
266 }
267
268 public List<Action> getCustomActions() {
269 return customActions;
270 }
271
272 public int getMaxIndex() {
273 return this.maxIndex;
274 }
275
276 public int getMinIndex() {
277 return this.minIndex;
278 }
279
280 public StrSubstitutor getStrSubstitutor() {
281 return strSubstitutor;
282 }
283
284 public boolean isStopCustomActionsOnError() {
285 return stopCustomActionsOnError;
286 }
287
288 public boolean isUseMax() {
289 return useMax;
290 }
291
292 private Action merge(final Action compressAction, final List<Action> custom, final boolean stopOnError) {
293 if (custom.isEmpty()) {
294 return compressAction;
295 }
296 if (compressAction == null) {
297 return new CompositeAction(custom, stopOnError);
298 }
299 final List<Action> all = new ArrayList<>();
300 all.add(compressAction);
301 all.addAll(custom);
302 return new CompositeAction(all, stopOnError);
303 }
304
305 private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
306 return useMax ? purgeAscending(lowIndex, highIndex, manager) : purgeDescending(lowIndex, highIndex, manager);
307 }
308
309
310
311
312
313
314
315
316
317
318 private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
319 final List<FileRenameAction> renames = new ArrayList<>();
320 final StringBuilder buf = new StringBuilder();
321
322
323 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, highIndex);
324 String highFilename = strSubstitutor.replace(buf);
325 final int suffixLength = suffixLength(highFilename);
326 int curMaxIndex = 0;
327
328 for (int i = highIndex; i >= lowIndex; i--) {
329 File toRename = new File(highFilename);
330 if (i == highIndex && toRename.exists()) {
331 curMaxIndex = highIndex;
332 } else if (curMaxIndex == 0 && toRename.exists()) {
333 curMaxIndex = i + 1;
334 break;
335 }
336
337 boolean isBase = false;
338
339 if (suffixLength > 0) {
340 final File toRenameBase = new File(highFilename.substring(0, highFilename.length() - suffixLength));
341
342 if (toRename.exists()) {
343 if (toRenameBase.exists()) {
344 LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} base of {}.",
345 toRenameBase, toRename);
346 toRenameBase.delete();
347 }
348 } else {
349 toRename = toRenameBase;
350 isBase = true;
351 }
352 }
353
354 if (toRename.exists()) {
355
356
357
358
359 if (i == lowIndex) {
360 LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} at low index {}: all slots full.",
361 toRename, i);
362 if (!toRename.delete()) {
363 return -1;
364 }
365
366 break;
367 }
368
369
370
371
372 buf.setLength(0);
373
374 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, i - 1);
375
376 final String lowFilename = strSubstitutor.replace(buf);
377 String renameTo = lowFilename;
378
379 if (isBase) {
380 renameTo = lowFilename.substring(0, lowFilename.length() - suffixLength);
381 }
382
383 renames.add(new FileRenameAction(toRename, new File(renameTo), true));
384 highFilename = lowFilename;
385 } else {
386 buf.setLength(0);
387
388 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, i - 1);
389
390 highFilename = strSubstitutor.replace(buf);
391 }
392 }
393 if (curMaxIndex == 0) {
394 curMaxIndex = lowIndex;
395 }
396
397
398
399
400 for (int i = renames.size() - 1; i >= 0; i--) {
401 final Action action = renames.get(i);
402 try {
403 LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {} of {}: {}",
404 i, renames.size(), action);
405 if (!action.execute()) {
406 return -1;
407 }
408 } catch (final Exception ex) {
409 LOGGER.warn("Exception during purge in RollingFileAppender", ex);
410 return -1;
411 }
412 }
413 return curMaxIndex;
414 }
415
416
417
418
419
420
421
422
423
424
425 private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
426 final List<FileRenameAction> renames = new ArrayList<>();
427 final StringBuilder buf = new StringBuilder();
428
429
430 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, lowIndex);
431
432 String lowFilename = strSubstitutor.replace(buf);
433 final int suffixLength = suffixLength(lowFilename);
434
435 for (int i = lowIndex; i <= highIndex; i++) {
436 File toRename = new File(lowFilename);
437 boolean isBase = false;
438
439 if (suffixLength > 0) {
440 final File toRenameBase = new File(lowFilename.substring(0, lowFilename.length() - suffixLength));
441
442 if (toRename.exists()) {
443 if (toRenameBase.exists()) {
444 LOGGER.debug("DefaultRolloverStrategy.purgeDescending deleting {} base of {}.",
445 toRenameBase, toRename);
446 toRenameBase.delete();
447 }
448 } else {
449 toRename = toRenameBase;
450 isBase = true;
451 }
452 }
453
454 if (toRename.exists()) {
455
456
457
458
459 if (i == highIndex) {
460 LOGGER.debug(
461 "DefaultRolloverStrategy.purgeDescending deleting {} at high index {}: all slots full.",
462 toRename, i);
463 if (!toRename.delete()) {
464 return -1;
465 }
466
467 break;
468 }
469
470
471
472
473 buf.setLength(0);
474
475 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, i + 1);
476
477 final String highFilename = strSubstitutor.replace(buf);
478 String renameTo = highFilename;
479
480 if (isBase) {
481 renameTo = highFilename.substring(0, highFilename.length() - suffixLength);
482 }
483
484 renames.add(new FileRenameAction(toRename, new File(renameTo), true));
485 lowFilename = highFilename;
486 } else {
487 break;
488 }
489 }
490
491
492
493
494 for (int i = renames.size() - 1; i >= 0; i--) {
495 final Action action = renames.get(i);
496 try {
497 LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {} of {}: {}",
498 i, renames.size(), action);
499 if (!action.execute()) {
500 return -1;
501 }
502 } catch (final Exception ex) {
503 LOGGER.warn("Exception during purge in RollingFileAppender", ex);
504 return -1;
505 }
506 }
507
508 return lowIndex;
509 }
510
511
512
513
514
515
516
517
518 @Override
519 public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
520 if (maxIndex < 0) {
521 return null;
522 }
523 final long startNanos = System.nanoTime();
524 final int fileIndex = purge(minIndex, maxIndex, manager);
525 if (fileIndex < 0) {
526 return null;
527 }
528 if (LOGGER.isTraceEnabled()) {
529 final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
530 LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis);
531 }
532 final StringBuilder buf = new StringBuilder(255);
533 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex);
534 final String currentFileName = manager.getFileName();
535
536 String renameTo = buf.toString();
537 final String compressedName = renameTo;
538 Action compressAction = null;
539
540 for (final FileExtensions ext : FileExtensions.values()) {
541 if (ext.isExtensionFor(renameTo)) {
542 renameTo = renameTo.substring(0, renameTo.length() - ext.length());
543 compressAction = ext.createCompressAction(renameTo, compressedName, true, compressionLevel);
544 break;
545 }
546 }
547
548 final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo), false);
549
550 final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
551 return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
552 }
553
554 private int suffixLength(final String lowFilename) {
555 for (final FileExtensions extension : FileExtensions.values()) {
556 if (extension.isExtensionFor(lowFilename)) {
557 return extension.length();
558 }
559 }
560 return 0;
561 }
562
563 @Override
564 public String toString() {
565 return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ')';
566 }
567
568 }