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ülcü*/
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 }