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     */
017    package org.apache.logging.log4j.core.appender.rolling;
018    
019    import org.apache.logging.log4j.Logger;
020    import org.apache.logging.log4j.core.appender.rolling.helper.Action;
021    import org.apache.logging.log4j.core.appender.rolling.helper.FileRenameAction;
022    import org.apache.logging.log4j.core.appender.rolling.helper.GZCompressAction;
023    import org.apache.logging.log4j.core.appender.rolling.helper.ZipCompressAction;
024    import org.apache.logging.log4j.core.config.Configuration;
025    import org.apache.logging.log4j.core.config.plugins.Plugin;
026    import org.apache.logging.log4j.core.config.plugins.PluginAttr;
027    import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
028    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
029    import org.apache.logging.log4j.core.lookup.StrSubstitutor;
030    import org.apache.logging.log4j.status.StatusLogger;
031    
032    import java.io.File;
033    import java.util.ArrayList;
034    import java.util.List;
035    
036    /**
037     * When rolling over, <code>FixedWindowRollingPolicy</code> renames files
038     * according to a fixed window algorithm as described below.
039     * <p/>
040     * <p>The <b>ActiveFileName</b> property, which is required, represents the name
041     * of the file where current logging output will be written.
042     * The <b>FileNamePattern</b>  option represents the file name pattern for the
043     * archived (rolled over) log files. If present, the <b>FileNamePattern</b>
044     * option must include an integer token, that is the string "%i" somewhere
045     * within the pattern.
046     * <p/>
047     * <p>Let <em>max</em> and <em>min</em> represent the values of respectively
048     * the <b>MaxIndex</b> and <b>MinIndex</b> options. Let "foo.log" be the value
049     * of the <b>ActiveFile</b> option and "foo.%i.log" the value of
050     * <b>FileNamePattern</b>. Then, when rolling over, the file
051     * <code>foo.<em>max</em>.log</code> will be deleted, the file
052     * <code>foo.<em>max-1</em>.log</code> will be renamed as
053     * <code>foo.<em>max</em>.log</code>, the file <code>foo.<em>max-2</em>.log</code>
054     * renamed as <code>foo.<em>max-1</em>.log</code>, and so on,
055     * the file <code>foo.<em>min+1</em>.log</code> renamed as
056     * <code>foo.<em>min+2</em>.log</code>. Lastly, the active file <code>foo.log</code>
057     * will be renamed as <code>foo.<em>min</em>.log</code> and a new active file name
058     * <code>foo.log</code> will be created.
059     * <p/>
060     * <p>Given that this rollover algorithm requires as many file renaming
061     * operations as the window size, large window sizes are discouraged. The
062     * current implementation will automatically reduce the window size to 12 when
063     * larger values are specified by the user.
064     */
065    @Plugin(name = "DefaultRolloverStrategy", type = "Core", printObject = true)
066    public class DefaultRolloverStrategy implements RolloverStrategy {
067        /**
068         * Allow subclasses access to the status logger without creating another instance.
069         */
070        protected static final Logger LOGGER = StatusLogger.getLogger();
071    
072        private static final int MIN_WINDOW_SIZE = 1;
073        private static final int DEFAULT_WINDOW_SIZE = 7;
074    
075        /**
076         * Index for oldest retained log file.
077         */
078        private final int maxIndex;
079    
080        /**
081         * Index for most recent log file.
082         */
083        private final int minIndex;
084    
085        private final StrSubstitutor subst;
086    
087        /**
088         * Constructs a new instance.
089         * @param min The minimum index.
090         * @param max The maximum index.
091         */
092        protected DefaultRolloverStrategy(int min, int max, StrSubstitutor subst) {
093            minIndex = min;
094            maxIndex = max;
095            this.subst = subst;
096        }
097    
098        /**
099         * Perform the rollover.
100         * @param manager The RollingFileManager name for current active log file.
101         * @return A RolloverDescription.
102         * @throws SecurityException if an error occurs.
103         */
104        public RolloverDescription rollover(RollingFileManager manager) throws SecurityException {
105            if (maxIndex >= 0) {
106                int purgeStart = minIndex;
107    
108                if (!purge(purgeStart, maxIndex, manager)) {
109                    return null;
110                }
111    
112                StringBuilder buf = new StringBuilder();
113                manager.getProcessor().formatFileName(buf, purgeStart);
114                String currentFileName = manager.getFileName();
115    
116                String renameTo = subst.replace(buf);
117                String compressedName = renameTo;
118                Action compressAction = null;
119    
120                if (renameTo.endsWith(".gz")) {
121                    renameTo = renameTo.substring(0, renameTo.length() - 3);
122                    compressAction = new GZCompressAction(new File(renameTo), new File(compressedName), true);
123                } else if (renameTo.endsWith(".zip")) {
124                    renameTo = renameTo.substring(0, renameTo.length() - 4);
125                    compressAction = new ZipCompressAction(new File(renameTo), new File(compressedName), true);
126                }
127    
128                FileRenameAction renameAction =
129                    new FileRenameAction(new File(currentFileName), new File(renameTo), false);
130    
131                return new RolloverDescriptionImpl(currentFileName, false, renameAction, compressAction);
132            }
133    
134            return null;
135        }
136    
137        /**
138         * Purge and rename old log files in preparation for rollover
139         *
140         * @param lowIndex  low index
141         * @param highIndex high index.  Log file associated with high index will be deleted if needed.
142         * @param manager The RollingFileManager
143         * @return true if purge was successful and rollover should be attempted.
144         */
145        private boolean purge(final int lowIndex, final int highIndex, RollingFileManager manager) {
146            int suffixLength = 0;
147    
148            List<FileRenameAction> renames = new ArrayList<FileRenameAction>();
149            StringBuilder buf = new StringBuilder();
150            manager.getProcessor().formatFileName(buf, lowIndex);
151    
152            String lowFilename = subst.replace(buf);
153    
154            if (lowFilename.endsWith(".gz")) {
155                suffixLength = 3;
156            } else if (lowFilename.endsWith(".zip")) {
157                suffixLength = 4;
158            }
159    
160            for (int i = lowIndex; i <= highIndex; i++) {
161                File toRename = new File(lowFilename);
162                boolean isBase = false;
163    
164                if (suffixLength > 0) {
165                    File toRenameBase =
166                        new File(lowFilename.substring(0, lowFilename.length() - suffixLength));
167    
168                    if (toRename.exists()) {
169                        if (toRenameBase.exists()) {
170                            toRenameBase.delete();
171                        }
172                    } else {
173                        toRename = toRenameBase;
174                        isBase = true;
175                    }
176                }
177    
178                if (toRename.exists()) {
179                    //
180                    //    if at upper index then
181                    //        attempt to delete last file
182                    //        if that fails then abandon purge
183                    if (i == highIndex) {
184                        if (!toRename.delete()) {
185                            return false;
186                        }
187    
188                        break;
189                    }
190    
191                    //
192                    //   if intermediate index
193                    //     add a rename action to the list
194                    buf.setLength(0);
195                    manager.getProcessor().formatFileName(buf, i + 1);
196    
197                    String highFilename = subst.replace(buf);
198                    String renameTo = highFilename;
199    
200                    if (isBase) {
201                        renameTo = highFilename.substring(0, highFilename.length() - suffixLength);
202                    }
203    
204                    renames.add(new FileRenameAction(toRename, new File(renameTo), true));
205                    lowFilename = highFilename;
206                } else {
207                    break;
208                }
209            }
210    
211            //
212            //   work renames backwards
213            //
214            for (int i = renames.size() - 1; i >= 0; i--) {
215                Action action = renames.get(i);
216    
217                try {
218                    if (!action.execute()) {
219                        return false;
220                    }
221                } catch (Exception ex) {
222                    LOGGER.warn("Exception during purge in RollingFileAppender", ex);
223                    return false;
224                }
225            }
226    
227            return true;
228        }
229    
230        @Override
231        public String toString() {
232            return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ")";
233        }
234    
235        /**
236         * Create the DefaultRolloverStrategy.
237         * @param max The maximum number of files to keep.
238         * @param min The minimum number of files to keep.
239         * @param config The Configuration.
240         * @return A DefaultRolloverStrategy.
241         */
242        @PluginFactory
243        public static DefaultRolloverStrategy createStrategy(@PluginAttr("max") String max,
244                                                             @PluginAttr("min") String min,
245                                                             @PluginConfiguration Configuration config) {
246    
247            int minIndex;
248            if (min != null) {
249                minIndex = Integer.parseInt(min);
250                if (minIndex < 1) {
251                    LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
252                    minIndex = MIN_WINDOW_SIZE;
253                }
254            } else {
255                minIndex = MIN_WINDOW_SIZE;
256            }
257            int maxIndex;
258            if (max != null) {
259                maxIndex = Integer.parseInt(max);
260                if (maxIndex < minIndex) {
261                    maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
262                    LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
263                }
264            } else {
265                maxIndex = DEFAULT_WINDOW_SIZE;
266            }
267            return new DefaultRolloverStrategy(minIndex, maxIndex, config.getSubst());
268        }
269    
270    }