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
240    /**
241     * Constructs a new instance.
242     *
243     * @param maxFiles The maximum number of files that match the date portion of the pattern to keep.
244     * @param customActions custom actions to perform asynchronously after rollover
245     * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
246     * @deprecated Since 2.9 Added tempCompressedFilePatternString parameter
247     */
248    @Deprecated
249    protected DirectWriteRolloverStrategy(final int maxFiles, final int compressionLevel,
250                                          final StrSubstitutor strSubstitutor, final Action[] customActions,
251                                          final boolean stopCustomActionsOnError) {
252        this(maxFiles, compressionLevel, strSubstitutor, customActions, stopCustomActionsOnError, null);
253    }
254
255    /**
256     * Constructs a new instance.
257     *
258     * @param maxFiles The maximum number of files that match the date portion of the pattern to keep.
259     * @param customActions custom actions to perform asynchronously after rollover
260     * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
261     * @param tempCompressedFilePatternString File pattern of the working file
262     *                                     used during compression, if null no temporary file are used
263     */
264    protected DirectWriteRolloverStrategy(final int maxFiles, final int compressionLevel,
265                                          final StrSubstitutor strSubstitutor, final Action[] customActions,
266                                          final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString) {
267        super(strSubstitutor);
268        this.maxFiles = maxFiles;
269        this.compressionLevel = compressionLevel;
270        this.stopCustomActionsOnError = stopCustomActionsOnError;
271        this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions);
272        this.tempCompressedFilePattern =
273                tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null;
274    }
275
276    public int getCompressionLevel() {
277        return this.compressionLevel;
278    }
279
280    public List<Action> getCustomActions() {
281        return customActions;
282    }
283
284    public int getMaxFiles() {
285        return this.maxFiles;
286    }
287
288    public boolean isStopCustomActionsOnError() {
289        return stopCustomActionsOnError;
290    }
291
292    public PatternProcessor getTempCompressedFilePattern() {
293        return tempCompressedFilePattern;
294    }
295
296    private int purge(final RollingFileManager manager) {
297        final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
298        LOGGER.debug("Found {} eligible files, max is  {}", eligibleFiles.size(), maxFiles);
299        while (eligibleFiles.size() >= maxFiles) {
300            try {
301                final Integer key = eligibleFiles.firstKey();
302                Files.delete(eligibleFiles.get(key));
303                eligibleFiles.remove(key);
304            } catch (final IOException ioe) {
305                LOGGER.error("Unable to delete {}", eligibleFiles.firstKey(), ioe);
306                break;
307            }
308        }
309        return eligibleFiles.size() > 0 ? eligibleFiles.lastKey() : 1;
310    }
311
312    @Override
313    public String getCurrentFileName(final RollingFileManager manager) {
314        if (currentFileName == null) {
315            final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
316            final int fileIndex = eligibleFiles.size() > 0 ? (nextIndex > 0 ? nextIndex : eligibleFiles.size()) : 1;
317            final StringBuilder buf = new StringBuilder(255);
318            manager.getPatternProcessor().formatFileName(strSubstitutor, buf, true, fileIndex);
319            final int suffixLength = suffixLength(buf.toString());
320            final String name = suffixLength > 0 ? buf.substring(0, buf.length() - suffixLength) : buf.toString();
321            currentFileName = name;
322        }
323        return currentFileName;
324    }
325
326    /**
327     * Performs the rollover.
328     *
329     * @param manager The RollingFileManager name for current active log file.
330     * @return A RolloverDescription.
331     * @throws SecurityException if an error occurs.
332     */
333    @Override
334    public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
335        LOGGER.debug("Rolling " + currentFileName);
336        if (maxFiles < 0) {
337            return null;
338        }
339        final long startNanos = System.nanoTime();
340        final int fileIndex = purge(manager);
341        if (LOGGER.isTraceEnabled()) {
342            final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
343            LOGGER.trace("DirectWriteRolloverStrategy.purge() took {} milliseconds", durationMillis);
344        }
345        Action compressAction = null;
346        final String sourceName = getCurrentFileName(manager);
347        String compressedName = sourceName;
348        currentFileName = null;
349        nextIndex = fileIndex + 1;
350        final FileExtension fileExtension = manager.getFileExtension();
351        if (fileExtension != null) {
352            compressedName += fileExtension.getExtension();            
353            if (tempCompressedFilePattern != null) {
354                final StringBuilder buf = new StringBuilder();
355                tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex);
356                final String tmpCompressedName = buf.toString();
357                final File tmpCompressedNameFile = new File(tmpCompressedName);
358                final File parentFile = tmpCompressedNameFile.getParentFile();
359                if (parentFile != null) {
360                    parentFile.mkdirs();
361                }
362                compressAction = new CompositeAction(
363                        Arrays.asList(fileExtension.createCompressAction(sourceName, tmpCompressedName,
364                                true, compressionLevel),
365                                new FileRenameAction(tmpCompressedNameFile,
366                                        new File(compressedName), true)),
367                        true);
368            } else {
369                compressAction = fileExtension.createCompressAction(sourceName, compressedName,
370                      true, compressionLevel);
371            }
372        }
373
374        if (compressAction != null && manager.isAttributeViewEnabled()) {
375            // Propagate posix attribute view to compressed file
376            // @formatter:off
377            final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder()
378                                                    .withBasePath(compressedName)
379                                                    .withFollowLinks(false)
380                                                    .withMaxDepth(1)
381                                                    .withPathConditions(new PathCondition[0])
382                                                    .withSubst(getStrSubstitutor())
383                                                    .withFilePermissions(manager.getFilePermissions())
384                                                    .withFileOwner(manager.getFileOwner())
385                                                    .withFileGroup(manager.getFileGroup())
386                                                    .build();
387            // @formatter:on
388            compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false);
389        }
390
391        final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
392        return new RolloverDescriptionImpl(sourceName, false, null, asyncAction);
393    }
394
395    @Override
396    public String toString() {
397        return "DirectWriteRolloverStrategy(maxFiles=" + maxFiles + ')';
398    }
399
400}