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
018
019
020package org.apache.log4j;
021
022import java.io.IOException;
023import java.io.File;
024import java.io.InterruptedIOException;
025import java.text.SimpleDateFormat;
026import java.util.Date;
027import java.util.GregorianCalendar;
028import java.util.Calendar;
029import java.util.TimeZone;
030import java.util.Locale;
031
032import org.apache.log4j.helpers.LogLog;
033import org.apache.log4j.spi.LoggingEvent;
034
035/**
036   DailyRollingFileAppender extends {@link FileAppender} so that the
037   underlying file is rolled over at a user chosen frequency.
038   
039   DailyRollingFileAppender has been observed to exhibit 
040   synchronization issues and data loss.  The log4j extras
041   companion includes alternatives which should be considered
042   for new deployments and which are discussed in the documentation
043   for org.apache.log4j.rolling.RollingFileAppender.
044
045   <p>The rolling schedule is specified by the <b>DatePattern</b>
046   option. This pattern should follow the {@link SimpleDateFormat}
047   conventions. In particular, you <em>must</em> escape literal text
048   within a pair of single quotes. A formatted version of the date
049   pattern is used as the suffix for the rolled file name.
050
051   <p>For example, if the <b>File</b> option is set to
052   <code>/foo/bar.log</code> and the <b>DatePattern</b> set to
053   <code>'.'yyyy-MM-dd</code>, on 2001-02-16 at midnight, the logging
054   file <code>/foo/bar.log</code> will be copied to
055   <code>/foo/bar.log.2001-02-16</code> and logging for 2001-02-17
056   will continue in <code>/foo/bar.log</code> until it rolls over
057   the next day.
058
059   <p>Is is possible to specify monthly, weekly, half-daily, daily,
060   hourly, or minutely rollover schedules.
061
062   <p><table border="1" cellpadding="2">
063   <tr>
064   <th>DatePattern</th>
065   <th>Rollover schedule</th>
066   <th>Example</th>
067
068   <tr>
069   <td><code>'.'yyyy-MM</code>
070   <td>Rollover at the beginning of each month</td>
071
072   <td>At midnight of May 31st, 2002 <code>/foo/bar.log</code> will be
073   copied to <code>/foo/bar.log.2002-05</code>. Logging for the month
074   of June will be output to <code>/foo/bar.log</code> until it is
075   also rolled over the next month.
076
077   <tr>
078   <td><code>'.'yyyy-ww</code>
079
080   <td>Rollover at the first day of each week. The first day of the
081   week depends on the locale.</td>
082
083   <td>Assuming the first day of the week is Sunday, on Saturday
084   midnight, June 9th 2002, the file <i>/foo/bar.log</i> will be
085   copied to <i>/foo/bar.log.2002-23</i>.  Logging for the 24th week
086   of 2002 will be output to <code>/foo/bar.log</code> until it is
087   rolled over the next week.
088
089   <tr>
090   <td><code>'.'yyyy-MM-dd</code>
091
092   <td>Rollover at midnight each day.</td>
093
094   <td>At midnight, on March 8th, 2002, <code>/foo/bar.log</code> will
095   be copied to <code>/foo/bar.log.2002-03-08</code>. Logging for the
096   9th day of March will be output to <code>/foo/bar.log</code> until
097   it is rolled over the next day.
098
099   <tr>
100   <td><code>'.'yyyy-MM-dd-a</code>
101
102   <td>Rollover at midnight and midday of each day.</td>
103
104   <td>At noon, on March 9th, 2002, <code>/foo/bar.log</code> will be
105   copied to <code>/foo/bar.log.2002-03-09-AM</code>. Logging for the
106   afternoon of the 9th will be output to <code>/foo/bar.log</code>
107   until it is rolled over at midnight.
108
109   <tr>
110   <td><code>'.'yyyy-MM-dd-HH</code>
111
112   <td>Rollover at the top of every hour.</td>
113
114   <td>At approximately 11:00.000 o'clock on March 9th, 2002,
115   <code>/foo/bar.log</code> will be copied to
116   <code>/foo/bar.log.2002-03-09-10</code>. Logging for the 11th hour
117   of the 9th of March will be output to <code>/foo/bar.log</code>
118   until it is rolled over at the beginning of the next hour.
119
120
121   <tr>
122   <td><code>'.'yyyy-MM-dd-HH-mm</code>
123
124   <td>Rollover at the beginning of every minute.</td>
125
126   <td>At approximately 11:23,000, on March 9th, 2001,
127   <code>/foo/bar.log</code> will be copied to
128   <code>/foo/bar.log.2001-03-09-10-22</code>. Logging for the minute
129   of 11:23 (9th of March) will be output to
130   <code>/foo/bar.log</code> until it is rolled over the next minute.
131
132   </table>
133
134   <p>Do not use the colon ":" character in anywhere in the
135   <b>DatePattern</b> option. The text before the colon is interpeted
136   as the protocol specificaion of a URL which is probably not what
137   you want.
138
139
140   @author Eirik Lygre
141   @author Ceki G&uuml;lc&uuml;*/
142public class DailyRollingFileAppender extends FileAppender {
143
144
145  // The code assumes that the following constants are in a increasing
146  // sequence.
147  static final int TOP_OF_TROUBLE=-1;
148  static final int TOP_OF_MINUTE = 0;
149  static final int TOP_OF_HOUR   = 1;
150  static final int HALF_DAY      = 2;
151  static final int TOP_OF_DAY    = 3;
152  static final int TOP_OF_WEEK   = 4;
153  static final int TOP_OF_MONTH  = 5;
154
155
156  /**
157     The date pattern. By default, the pattern is set to
158     "'.'yyyy-MM-dd" meaning daily rollover.
159   */
160  private String datePattern = "'.'yyyy-MM-dd";
161
162  /**
163     The log file will be renamed to the value of the
164     scheduledFilename variable when the next interval is entered. For
165     example, if the rollover period is one hour, the log file will be
166     renamed to the value of "scheduledFilename" at the beginning of
167     the next hour. 
168
169     The precise time when a rollover occurs depends on logging
170     activity. 
171  */
172  private String scheduledFilename;
173
174  /**
175     The next time we estimate a rollover should occur. */
176  private long nextCheck = System.currentTimeMillis () - 1;
177
178  Date now = new Date();
179
180  SimpleDateFormat sdf;
181
182  RollingCalendar rc = new RollingCalendar();
183
184  int checkPeriod = TOP_OF_TROUBLE;
185
186  // The gmtTimeZone is used only in computeCheckPeriod() method.
187  static final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
188
189
190  /**
191     The default constructor does nothing. */
192  public DailyRollingFileAppender() {
193  }
194
195  /**
196    Instantiate a <code>DailyRollingFileAppender</code> and open the
197    file designated by <code>filename</code>. The opened filename will
198    become the ouput destination for this appender.
199
200    */
201  public DailyRollingFileAppender (Layout layout, String filename,
202                                   String datePattern) throws IOException {
203    super(layout, filename, true);
204    this.datePattern = datePattern;
205    activateOptions();
206  }
207
208  /**
209     The <b>DatePattern</b> takes a string in the same format as
210     expected by {@link SimpleDateFormat}. This options determines the
211     rollover schedule.
212   */
213  public void setDatePattern(String pattern) {
214    datePattern = pattern;
215  }
216
217  /** Returns the value of the <b>DatePattern</b> option. */
218  public String getDatePattern() {
219    return datePattern;
220  }
221
222  public void activateOptions() {
223    super.activateOptions();
224    if(datePattern != null && fileName != null) {
225      now.setTime(System.currentTimeMillis());
226      sdf = new SimpleDateFormat(datePattern);
227      int type = computeCheckPeriod();
228      printPeriodicity(type);
229      rc.setType(type);
230      File file = new File(fileName);
231      scheduledFilename = fileName+sdf.format(new Date(file.lastModified()));
232
233    } else {
234      LogLog.error("Either File or DatePattern options are not set for appender ["
235                   +name+"].");
236    }
237  }
238
239  void printPeriodicity(int type) {
240    switch(type) {
241    case TOP_OF_MINUTE:
242      LogLog.debug("Appender ["+name+"] to be rolled every minute.");
243      break;
244    case TOP_OF_HOUR:
245      LogLog.debug("Appender ["+name
246                   +"] to be rolled on top of every hour.");
247      break;
248    case HALF_DAY:
249      LogLog.debug("Appender ["+name
250                   +"] to be rolled at midday and midnight.");
251      break;
252    case TOP_OF_DAY:
253      LogLog.debug("Appender ["+name
254                   +"] to be rolled at midnight.");
255      break;
256    case TOP_OF_WEEK:
257      LogLog.debug("Appender ["+name
258                   +"] to be rolled at start of week.");
259      break;
260    case TOP_OF_MONTH:
261      LogLog.debug("Appender ["+name
262                   +"] to be rolled at start of every month.");
263      break;
264    default:
265      LogLog.warn("Unknown periodicity for appender ["+name+"].");
266    }
267  }
268
269
270  // This method computes the roll over period by looping over the
271  // periods, starting with the shortest, and stopping when the r0 is
272  // different from from r1, where r0 is the epoch formatted according
273  // the datePattern (supplied by the user) and r1 is the
274  // epoch+nextMillis(i) formatted according to datePattern. All date
275  // formatting is done in GMT and not local format because the test
276  // logic is based on comparisons relative to 1970-01-01 00:00:00
277  // GMT (the epoch).
278
279  int computeCheckPeriod() {
280    RollingCalendar rollingCalendar = new RollingCalendar(gmtTimeZone, Locale.getDefault());
281    // set sate to 1970-01-01 00:00:00 GMT
282    Date epoch = new Date(0);
283    if(datePattern != null) {
284      for(int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) {
285        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
286        simpleDateFormat.setTimeZone(gmtTimeZone); // do all date formatting in GMT
287        String r0 = simpleDateFormat.format(epoch);
288        rollingCalendar.setType(i);
289        Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
290        String r1 =  simpleDateFormat.format(next);
291        //System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);
292        if(r0 != null && r1 != null && !r0.equals(r1)) {
293          return i;
294        }
295      }
296    }
297    return TOP_OF_TROUBLE; // Deliberately head for trouble...
298  }
299
300  /**
301     Rollover the current file to a new file.
302  */
303  void rollOver() throws IOException {
304
305    /* Compute filename, but only if datePattern is specified */
306    if (datePattern == null) {
307      errorHandler.error("Missing DatePattern option in rollOver().");
308      return;
309    }
310
311    String datedFilename = fileName+sdf.format(now);
312    // It is too early to roll over because we are still within the
313    // bounds of the current interval. Rollover will occur once the
314    // next interval is reached.
315    if (scheduledFilename.equals(datedFilename)) {
316      return;
317    }
318
319    // close current file, and rename it to datedFilename
320    this.closeFile();
321
322    File target  = new File(scheduledFilename);
323    if (target.exists()) {
324      target.delete();
325    }
326
327    File file = new File(fileName);
328    boolean result = file.renameTo(target);
329    if(result) {
330      LogLog.debug(fileName +" -> "+ scheduledFilename);
331    } else {
332      LogLog.error("Failed to rename ["+fileName+"] to ["+scheduledFilename+"].");
333    }
334
335    try {
336      // This will also close the file. This is OK since multiple
337      // close operations are safe.
338      this.setFile(fileName, true, this.bufferedIO, this.bufferSize);
339    }
340    catch(IOException e) {
341      errorHandler.error("setFile("+fileName+", true) call failed.");
342    }
343    scheduledFilename = datedFilename;
344  }
345
346  /**
347   * This method differentiates DailyRollingFileAppender from its
348   * super class.
349   *
350   * <p>Before actually logging, this method will check whether it is
351   * time to do a rollover. If it is, it will schedule the next
352   * rollover time and then rollover.
353   * */
354  protected void subAppend(LoggingEvent event) {
355    long n = System.currentTimeMillis();
356    if (n >= nextCheck) {
357      now.setTime(n);
358      nextCheck = rc.getNextCheckMillis(now);
359      try {
360        rollOver();
361      }
362      catch(IOException ioe) {
363          if (ioe instanceof InterruptedIOException) {
364              Thread.currentThread().interrupt();
365          }
366              LogLog.error("rollOver() failed.", ioe);
367      }
368    }
369    super.subAppend(event);
370   }
371}
372
373/**
374 *  RollingCalendar is a helper class to DailyRollingFileAppender.
375 *  Given a periodicity type and the current time, it computes the
376 *  start of the next interval.  
377 * */
378class RollingCalendar extends GregorianCalendar {
379  private static final long serialVersionUID = -3560331770601814177L;
380
381  int type = DailyRollingFileAppender.TOP_OF_TROUBLE;
382
383  RollingCalendar() {
384    super();
385  }  
386
387  RollingCalendar(TimeZone tz, Locale locale) {
388    super(tz, locale);
389  }  
390
391  void setType(int type) {
392    this.type = type;
393  }
394
395  public long getNextCheckMillis(Date now) {
396    return getNextCheckDate(now).getTime();
397  }
398
399  public Date getNextCheckDate(Date now) {
400    this.setTime(now);
401
402    switch(type) {
403    case DailyRollingFileAppender.TOP_OF_MINUTE:
404        this.set(Calendar.SECOND, 0);
405        this.set(Calendar.MILLISECOND, 0);
406        this.add(Calendar.MINUTE, 1);
407        break;
408    case DailyRollingFileAppender.TOP_OF_HOUR:
409        this.set(Calendar.MINUTE, 0);
410        this.set(Calendar.SECOND, 0);
411        this.set(Calendar.MILLISECOND, 0);
412        this.add(Calendar.HOUR_OF_DAY, 1);
413        break;
414    case DailyRollingFileAppender.HALF_DAY:
415        this.set(Calendar.MINUTE, 0);
416        this.set(Calendar.SECOND, 0);
417        this.set(Calendar.MILLISECOND, 0);
418        int hour = get(Calendar.HOUR_OF_DAY);
419        if(hour < 12) {
420          this.set(Calendar.HOUR_OF_DAY, 12);
421        } else {
422          this.set(Calendar.HOUR_OF_DAY, 0);
423          this.add(Calendar.DAY_OF_MONTH, 1);
424        }
425        break;
426    case DailyRollingFileAppender.TOP_OF_DAY:
427        this.set(Calendar.HOUR_OF_DAY, 0);
428        this.set(Calendar.MINUTE, 0);
429        this.set(Calendar.SECOND, 0);
430        this.set(Calendar.MILLISECOND, 0);
431        this.add(Calendar.DATE, 1);
432        break;
433    case DailyRollingFileAppender.TOP_OF_WEEK:
434        this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek());
435        this.set(Calendar.HOUR_OF_DAY, 0);
436        this.set(Calendar.MINUTE, 0);
437        this.set(Calendar.SECOND, 0);
438        this.set(Calendar.MILLISECOND, 0);
439        this.add(Calendar.WEEK_OF_YEAR, 1);
440        break;
441    case DailyRollingFileAppender.TOP_OF_MONTH:
442        this.set(Calendar.DATE, 1);
443        this.set(Calendar.HOUR_OF_DAY, 0);
444        this.set(Calendar.MINUTE, 0);
445        this.set(Calendar.SECOND, 0);
446        this.set(Calendar.MILLISECOND, 0);
447        this.add(Calendar.MONTH, 1);
448        break;
449    default:
450        throw new IllegalStateException("Unknown periodicity type.");
451    }
452    return getTime();
453  }
454}