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