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 }