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
    18
    19
    20  package org.apache.log4j;
    21
    22  import java.io.IOException;
    23  import java.io.File;
    24  import java.io.InterruptedIOException;
    25  import java.text.SimpleDateFormat;
    26  import java.util.Date;
    27  import java.util.GregorianCalendar;
    28  import java.util.Calendar;
    29  import java.util.TimeZone;
    30  import java.util.Locale;
    31
    32  import org.apache.log4j.helpers.LogLog;
    33  import org.apache.log4j.spi.LoggingEvent;
    34
    35  /**
    36     DailyRollingFileAppender extends {@link FileAppender} so that the
    37     underlying file is rolled over at a user chosen frequency.
    38
    39     DailyRollingFileAppender has been observed to exhibit
    40     synchronization issues and data loss.  The log4j extras
    41     companion includes alternatives which should be considered
    42     for new deployments and which are discussed in the documentation
    43     for org.apache.log4j.rolling.RollingFileAppender.
    44
    45     <p>The rolling schedule is specified by the <b>DatePattern</b>
    46     option. This pattern should follow the {@link SimpleDateFormat}
    47     conventions. In particular, you <em>must</em> escape literal text
    48     within a pair of single quotes. A formatted version of the date
    49     pattern is used as the suffix for the rolled file name.
    50
    51     <p>For example, if the <b>File</b> option is set to
    52     <code>/foo/bar.log</code> and the <b>DatePattern</b> set to
    53     <code>'.'yyyy-MM-dd</code>, on 2001-02-16 at midnight, the logging
    54     file <code>/foo/bar.log</code> will be copied to
    55     <code>/foo/bar.log.2001-02-16</code> and logging for 2001-02-17
    56     will continue in <code>/foo/bar.log</code> until it rolls over
    57     the next day.
    58
    59     <p>Is is possible to specify monthly, weekly, half-daily, daily,
    60     hourly, or minutely rollover schedules.
    61
    62     <p><table border="1" cellpadding="2">
    63     <tr>
    64     <th>DatePattern</th>
    65     <th>Rollover schedule</th>
    66     <th>Example</th>
    67
    68     <tr>
    69     <td><code>'.'yyyy-MM</code>
    70     <td>Rollover at the beginning of each month</td>
    71
    72     <td>At midnight of May 31st, 2002 <code>/foo/bar.log</code> will be
    73     copied to <code>/foo/bar.log.2002-05</code>. Logging for the month
    74     of June will be output to <code>/foo/bar.log</code> until it is
    75     also rolled over the next month.
    76
    77     <tr>
    78     <td><code>'.'yyyy-ww</code>
    79
    80     <td>Rollover at the first day of each week. The first day of the
    81     week depends on the locale.</td>
    82
    83     <td>Assuming the first day of the week is Sunday, on Saturday
    84     midnight, June 9th 2002, the file <i>/foo/bar.log</i> will be
    85     copied to <i>/foo/bar.log.2002-23</i>.  Logging for the 24th week
    86     of 2002 will be output to <code>/foo/bar.log</code> until it is
    87     rolled over the next week.
    88
    89     <tr>
    90     <td><code>'.'yyyy-MM-dd</code>
    91
    92     <td>Rollover at midnight each day.</td>
    93
    94     <td>At midnight, on March 8th, 2002, <code>/foo/bar.log</code> will
    95     be copied to <code>/foo/bar.log.2002-03-08</code>. Logging for the
    96     9th day of March will be output to <code>/foo/bar.log</code> until
    97     it is rolled over the next day.
    98
    99     <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;*/
    142 public 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  * */
    378 class 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 }