View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.appender.rolling;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.nio.file.Files;
22  import java.nio.file.Path;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.List;
26  import java.util.SortedMap;
27  import java.util.concurrent.TimeUnit;
28  import java.util.zip.Deflater;
29  
30  import org.apache.logging.log4j.core.Core;
31  import org.apache.logging.log4j.core.appender.rolling.action.Action;
32  import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction;
33  import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
34  import org.apache.logging.log4j.core.appender.rolling.action.PathCondition;
35  import org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction;
36  import org.apache.logging.log4j.core.config.Configuration;
37  import org.apache.logging.log4j.core.config.plugins.Plugin;
38  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
39  import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
40  import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
41  import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
42  import org.apache.logging.log4j.core.config.plugins.PluginElement;
43  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
44  import org.apache.logging.log4j.core.lookup.StrSubstitutor;
45  import org.apache.logging.log4j.core.util.Integers;
46  
47  /**
48   * When rolling over, <code>DirectWriteRolloverStrategy</code> writes directly to the file as resolved by the file
49   * pattern. Files will be renamed files according to an algorithm as described below.
50   *
51   * <p>
52   * The DirectWriteRolloverStrategy uses similar logic as DefaultRolloverStrategy to determine the file name based
53   * on the file pattern, however the DirectWriteRolloverStrategy writes directly to a file and does not rename it
54   * during rollover, except if it is compressed, in which case it will add the appropriate file extension.
55   * </p>
56   *
57   * @since 2.8
58   */
59  @Plugin(name = "DirectWriteRolloverStrategy", category = Core.CATEGORY_NAME, printObject = true)
60  public class DirectWriteRolloverStrategy extends AbstractRolloverStrategy implements DirectFileRolloverStrategy {
61  
62      private static final int DEFAULT_MAX_FILES = 7;
63  
64      /**
65       * Builds DirectWriteRolloverStrategy instances.
66       */
67      public static class Builder implements org.apache.logging.log4j.core.util.Builder<DirectWriteRolloverStrategy> {
68          @PluginBuilderAttribute("maxFiles")
69          private String maxFiles;
70  
71          @PluginBuilderAttribute("compressionLevel")
72          private String compressionLevelStr;
73  
74          @PluginElement("Actions")
75          private Action[] customActions;
76  
77          @PluginBuilderAttribute(value = "stopCustomActionsOnError")
78          private boolean stopCustomActionsOnError = true;
79  
80          @PluginBuilderAttribute(value = "tempCompressedFilePattern")
81          private String tempCompressedFilePattern;
82  
83          @PluginConfiguration
84          private Configuration config;
85  
86          @Override
87          public DirectWriteRolloverStrategy build() {
88              int maxIndex = Integer.MAX_VALUE;
89              if (maxFiles != null) {
90                  maxIndex = Integer.parseInt(maxFiles);
91                  if (maxIndex < 0) {
92                      maxIndex = Integer.MAX_VALUE;
93                  } else if (maxIndex < 2) {
94                      LOGGER.error("Maximum files too small. Limited to " + DEFAULT_MAX_FILES);
95                      maxIndex = DEFAULT_MAX_FILES;
96                  }
97              }
98              final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
99              return new DirectWriteRolloverStrategy(maxIndex, compressionLevel, config.getStrSubstitutor(),
100                     customActions, stopCustomActionsOnError, tempCompressedFilePattern);
101         }
102 
103         public String getMaxFiles() {
104             return maxFiles;
105         }
106 
107         /**
108          * Defines the maximum number of files to keep.
109          *
110          * @param maxFiles The maximum number of files that match the date portion of the pattern to keep.
111          * @return This builder for chaining convenience
112          */
113         public Builder withMaxFiles(final String maxFiles) {
114             this.maxFiles = maxFiles;
115             return this;
116         }
117 
118         public String getCompressionLevelStr() {
119             return compressionLevelStr;
120         }
121 
122         /**
123          * Defines compression level.
124          *
125          * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
126          * @return This builder for chaining convenience
127          */
128         public Builder withCompressionLevelStr(final String compressionLevelStr) {
129             this.compressionLevelStr = compressionLevelStr;
130             return this;
131         }
132 
133         public Action[] getCustomActions() {
134             return customActions;
135         }
136 
137         /**
138          * Defines custom actions.
139          *
140          * @param customActions custom actions to perform asynchronously after rollover
141          * @return This builder for chaining convenience
142          */
143         public Builder withCustomActions(final Action[] customActions) {
144             this.customActions = customActions;
145             return this;
146         }
147 
148         public boolean isStopCustomActionsOnError() {
149             return stopCustomActionsOnError;
150         }
151 
152         /**
153          * Defines whether to stop executing asynchronous actions if an error occurs.
154          *
155          * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
156          * @return This builder for chaining convenience
157          */
158         public Builder withStopCustomActionsOnError(final boolean stopCustomActionsOnError) {
159             this.stopCustomActionsOnError = stopCustomActionsOnError;
160             return this;
161         }
162 
163         public String getTempCompressedFilePattern() {
164             return tempCompressedFilePattern;
165         }
166 
167         /**
168          * Defines temporary compression file pattern.
169          *
170          * @param tempCompressedFilePattern File pattern of the working file pattern used during compression, if null no temporary file are used
171          * @return This builder for chaining convenience
172          */
173         public Builder withTempCompressedFilePattern(final String tempCompressedFilePattern) {
174             this.tempCompressedFilePattern = tempCompressedFilePattern;
175             return this;
176         }
177 
178         public Configuration getConfig() {
179             return config;
180         }
181 
182         /**
183          * Defines configuration.
184          *
185          * @param config The Configuration.
186          * @return This builder for chaining convenience
187          */
188         public Builder withConfig(final Configuration config) {
189             this.config = config;
190             return this;
191         }
192     }
193 
194     @PluginBuilderFactory
195     public static Builder newBuilder() {
196         return new Builder();
197     }
198 
199     /**
200      * Creates the DirectWriteRolloverStrategy.
201      *
202      * @param maxFiles The maximum number of files that match the date portion of the pattern to keep.
203      * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
204      * @param customActions custom actions to perform asynchronously after rollover
205      * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
206      * @param config The Configuration.
207      * @return A DirectWriteRolloverStrategy.
208      * @deprecated Since 2.9 Usage of Builder API is preferable
209      */
210     @Deprecated
211     @PluginFactory
212     public static DirectWriteRolloverStrategy createStrategy(
213             // @formatter:off
214             @PluginAttribute("maxFiles") final String maxFiles,
215             @PluginAttribute("compressionLevel") final String compressionLevelStr,
216             @PluginElement("Actions") final Action[] customActions,
217             @PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true)
218                     final boolean stopCustomActionsOnError,
219             @PluginConfiguration final Configuration config) {
220             return newBuilder().withMaxFiles(maxFiles)
221                     .withCompressionLevelStr(compressionLevelStr)
222                     .withCustomActions(customActions)
223                     .withStopCustomActionsOnError(stopCustomActionsOnError)
224                     .withConfig(config)
225                     .build();
226             // @formatter:on
227     }
228 
229     /**
230      * Index for most recent log file.
231      */
232     private final int maxFiles;
233     private final int compressionLevel;
234     private final List<Action> customActions;
235     private final boolean stopCustomActionsOnError;
236     private volatile String currentFileName;
237     private int nextIndex = -1;
238     private final PatternProcessor tempCompressedFilePattern;
239     private volatile boolean usePrevTime = false;
240 
241     /**
242      * Constructs a new instance.
243      *
244      * @param maxFiles The maximum number of files that match the date portion of the pattern to keep.
245      * @param customActions custom actions to perform asynchronously after rollover
246      * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
247      * @deprecated Since 2.9 Added tempCompressedFilePatternString parameter
248      */
249     @Deprecated
250     protected DirectWriteRolloverStrategy(final int maxFiles, final int compressionLevel,
251                                           final StrSubstitutor strSubstitutor, final Action[] customActions,
252                                           final boolean stopCustomActionsOnError) {
253         this(maxFiles, compressionLevel, strSubstitutor, customActions, stopCustomActionsOnError, null);
254     }
255 
256     /**
257      * Constructs a new instance.
258      *
259      * @param maxFiles The maximum number of files that match the date portion of the pattern to keep.
260      * @param customActions custom actions to perform asynchronously after rollover
261      * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
262      * @param tempCompressedFilePatternString File pattern of the working file
263      *                                     used during compression, if null no temporary file are used
264      */
265     protected DirectWriteRolloverStrategy(final int maxFiles, final int compressionLevel,
266                                           final StrSubstitutor strSubstitutor, final Action[] customActions,
267                                           final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString) {
268         super(strSubstitutor);
269         this.maxFiles = maxFiles;
270         this.compressionLevel = compressionLevel;
271         this.stopCustomActionsOnError = stopCustomActionsOnError;
272         this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions);
273         this.tempCompressedFilePattern =
274                 tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null;
275     }
276 
277     public int getCompressionLevel() {
278         return this.compressionLevel;
279     }
280 
281     public List<Action> getCustomActions() {
282         return customActions;
283     }
284 
285     public int getMaxFiles() {
286         return this.maxFiles;
287     }
288 
289     public boolean isStopCustomActionsOnError() {
290         return stopCustomActionsOnError;
291     }
292 
293     public PatternProcessor getTempCompressedFilePattern() {
294         return tempCompressedFilePattern;
295     }
296 
297     private int purge(final RollingFileManager manager) {
298         final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
299         LOGGER.debug("Found {} eligible files, max is  {}", eligibleFiles.size(), maxFiles);
300         while (eligibleFiles.size() >= maxFiles) {
301             try {
302                 final Integer key = eligibleFiles.firstKey();
303                 Files.delete(eligibleFiles.get(key));
304                 eligibleFiles.remove(key);
305             } catch (final IOException ioe) {
306                 LOGGER.error("Unable to delete {}", eligibleFiles.firstKey(), ioe);
307                 break;
308             }
309         }
310         return eligibleFiles.size() > 0 ? eligibleFiles.lastKey() : 1;
311     }
312 
313     @Override
314     public String getCurrentFileName(final RollingFileManager manager) {
315         if (currentFileName == null) {
316             final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
317             final int fileIndex = eligibleFiles.size() > 0 ? (nextIndex > 0 ? nextIndex : eligibleFiles.size()) : 1;
318             final StringBuilder buf = new StringBuilder(255);
319             manager.getPatternProcessor().formatFileName(strSubstitutor, buf, true, fileIndex);
320             final int suffixLength = suffixLength(buf.toString());
321             final String name = suffixLength > 0 ? buf.substring(0, buf.length() - suffixLength) : buf.toString();
322             currentFileName = name;
323         }
324         return currentFileName;
325     }
326 
327     @Override
328     public void clearCurrentFileName() {
329         currentFileName = null;
330     }
331 
332     /**
333      * Performs the rollover.
334      *
335      * @param manager The RollingFileManager name for current active log file.
336      * @return A RolloverDescription.
337      * @throws SecurityException if an error occurs.
338      */
339     @Override
340     public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
341         LOGGER.debug("Rolling " + currentFileName);
342         if (maxFiles < 0) {
343             return null;
344         }
345         final long startNanos = System.nanoTime();
346         final int fileIndex = purge(manager);
347         if (LOGGER.isTraceEnabled()) {
348             final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
349             LOGGER.trace("DirectWriteRolloverStrategy.purge() took {} milliseconds", durationMillis);
350         }
351         Action compressAction = null;
352         final String sourceName = getCurrentFileName(manager);
353         String compressedName = sourceName;
354         currentFileName = null;
355         nextIndex = fileIndex + 1;
356         final FileExtension fileExtension = manager.getFileExtension();
357         if (fileExtension != null) {
358             compressedName += fileExtension.getExtension();
359             if (tempCompressedFilePattern != null) {
360                 final StringBuilder buf = new StringBuilder();
361                 tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex);
362                 final String tmpCompressedName = buf.toString();
363                 final File tmpCompressedNameFile = new File(tmpCompressedName);
364                 final File parentFile = tmpCompressedNameFile.getParentFile();
365                 if (parentFile != null) {
366                     parentFile.mkdirs();
367                 }
368                 compressAction = new CompositeAction(
369                         Arrays.asList(fileExtension.createCompressAction(sourceName, tmpCompressedName,
370                                 true, compressionLevel),
371                                 new FileRenameAction(tmpCompressedNameFile,
372                                         new File(compressedName), true)),
373                         true);
374             } else {
375                 compressAction = fileExtension.createCompressAction(sourceName, compressedName,
376                       true, compressionLevel);
377             }
378         }
379 
380         if (compressAction != null && manager.isAttributeViewEnabled()) {
381             // Propagate posix attribute view to compressed file
382             // @formatter:off
383             final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder()
384                                                     .withBasePath(compressedName)
385                                                     .withFollowLinks(false)
386                                                     .withMaxDepth(1)
387                                                     .withPathConditions(new PathCondition[0])
388                                                     .withSubst(getStrSubstitutor())
389                                                     .withFilePermissions(manager.getFilePermissions())
390                                                     .withFileOwner(manager.getFileOwner())
391                                                     .withFileGroup(manager.getFileGroup())
392                                                     .build();
393             // @formatter:on
394             compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false);
395         }
396 
397         final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
398         return new RolloverDescriptionImpl(sourceName, false, null, asyncAction);
399     }
400 
401     @Override
402     public String toString() {
403         return "DirectWriteRolloverStrategy(maxFiles=" + maxFiles + ')';
404     }
405 
406 }