001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.core.appender.rolling;
018
019import java.io.File;
020import java.io.IOException;
021import java.nio.file.Files;
022import java.nio.file.Path;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.List;
026import java.util.SortedMap;
027import java.util.concurrent.TimeUnit;
028import java.util.zip.Deflater;
029
030import org.apache.logging.log4j.core.Core;
031import org.apache.logging.log4j.core.appender.rolling.action.Action;
032import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction;
033import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
034import org.apache.logging.log4j.core.appender.rolling.action.PathCondition;
035import org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction;
036import org.apache.logging.log4j.core.config.Configuration;
037import org.apache.logging.log4j.core.config.plugins.Plugin;
038import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
039import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
040import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
041import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
042import org.apache.logging.log4j.core.config.plugins.PluginElement;
043import org.apache.logging.log4j.core.config.plugins.PluginFactory;
044import org.apache.logging.log4j.core.lookup.StrSubstitutor;
045import org.apache.logging.log4j.core.util.Integers;
046
047/**
048 * When rolling over, <code>DirectWriteRolloverStrategy</code> writes directly to the file as resolved by the file
049 * pattern. Files will be renamed files according to an algorithm as described below.
050 *
051 * <p>
052 * The DirectWriteRolloverStrategy uses similar logic as DefaultRolloverStrategy to determine the file name based
053 * on the file pattern, however the DirectWriteRolloverStrategy writes directly to a file and does not rename it
054 * during rollover, except if it is compressed, in which case it will add the appropriate file extension.
055 * </p>
056 *
057 * @since 2.8
058 */
059@Plugin(name = "DirectWriteRolloverStrategy", category = Core.CATEGORY_NAME, printObject = true)
060public class DirectWriteRolloverStrategy extends AbstractRolloverStrategy implements DirectFileRolloverStrategy {
061
062    private static final int DEFAULT_MAX_FILES = 7;
063
064    /**
065     * Builds DirectWriteRolloverStrategy instances.
066     */
067    public static class Builder implements org.apache.logging.log4j.core.util.Builder<DirectWriteRolloverStrategy> {
068        @PluginBuilderAttribute("maxFiles")
069        private String maxFiles;
070
071        @PluginBuilderAttribute("compressionLevel")
072        private String compressionLevelStr;
073
074        @PluginElement("Actions")
075        private Action[] customActions;
076
077        @PluginBuilderAttribute(value = "stopCustomActionsOnError")
078        private boolean stopCustomActionsOnError = true;
079
080        @PluginBuilderAttribute(value = "tempCompressedFilePattern")
081        private String tempCompressedFilePattern;
082
083        @PluginConfiguration
084        private Configuration config;
085
086        @Override
087        public DirectWriteRolloverStrategy build() {
088            int maxIndex = Integer.MAX_VALUE;
089            if (maxFiles != null) {
090                maxIndex = Integer.parseInt(maxFiles);
091                if (maxIndex < 0) {
092                    maxIndex = Integer.MAX_VALUE;
093                } else if (maxIndex < 2) {
094                    LOGGER.error("Maximum files too small. Limited to " + DEFAULT_MAX_FILES);
095                    maxIndex = DEFAULT_MAX_FILES;
096                }
097            }
098            final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
099            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}