blob: fdc49db4bab48e4b3b9f5952f8e6c8cf3418c67c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Portions copyright (C) The Apache Software Foundation. All rights reserved.
*
* This software is published under the terms of the Apache Software
* License version 1.1, a copy of which has been included with this
* distribution in the LICENSE.txt file. */
package org.apache.hadoop.chukwa.inputtools.log4j;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Pattern;
import org.apache.hadoop.chukwa.datacollection.controller.ChukwaAgentController;
import org.apache.hadoop.chukwa.datacollection.controller.ClientFinalizer;
import org.apache.hadoop.chukwa.util.AdaptorNamingUtils;
import org.apache.hadoop.chukwa.util.RecordConstants;
import org.apache.log4j.FileAppender;
import org.apache.log4j.Layout;
import org.apache.log4j.Logger;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.LoggingEvent;
/**
ChukwaDailyRollingFileAppender is a slightly modified version of
DailyRollingFileAppender, with modified versions of its
<code>subAppend()</code> and <code>rollOver()</code> functions.
We would have preferred to sub-class DailyRollingFileAppender but
its authors clearly did not intend that to be a viable option since
they made too much of the class private or package-private
DailyRollingFileAppender extends {@link FileAppender} so that the
underlying file is rolled over at a user chosen frequency.
<p>The rolling schedule is specified by the <b>DatePattern</b>
option. This pattern should follow the {@link SimpleDateFormat}
conventions. In particular, you <em>must</em> escape literal text
within a pair of single quotes. A formatted version of the date
pattern is used as the suffix for the rolled file name.</p>
<p>For example, if the <b>File</b> option is set to
<code>/foo/bar.log</code> and the <b>DatePattern</b> set to
<code>'.'yyyy-MM-dd</code>, on 2001-02-16 at midnight, the logging
file <code>/foo/bar.log</code> will be copied to
<code>/foo/bar.log.2001-02-16</code> and logging for 2001-02-17
will continue in <code>/foo/bar.log</code> until it rolls over
the next day.</p>
<p>Is is possible to specify monthly, weekly, half-daily, daily,
hourly, or minutely rollover schedules.</p>
<table border="1" cellpadding="2" summary="">
<tr>
<th>DatePattern</th>
<th>Rollover schedule</th>
<th>Example</th></tr>
<tr>
<td><code>'.'yyyy-MM</code>
<td>Rollover at the beginning of each month</td>
<td>At midnight of May 31st, 2002 <code>/foo/bar.log</code> will be
copied to <code>/foo/bar.log.2002-05</code>. Logging for the month
of June will be output to <code>/foo/bar.log</code> until it is
also rolled over the next month.</td></tr>
<tr>
<td><code>'.'yyyy-ww</code>
<td>Rollover at the first day of each week. The first day of the
week depends on the locale.</td>
<td>Assuming the first day of the week is Sunday, on Saturday
midnight, June 9th 2002, the file <i>/foo/bar.log</i> will be
copied to <i>/foo/bar.log.2002-23</i>. Logging for the 24th week
of 2002 will be output to <code>/foo/bar.log</code> until it is
rolled over the next week.</td></tr>
<tr>
<td><code>'.'yyyy-MM-dd</code>
<td>Rollover at midnight each day.</td>
<td>At midnight, on March 8th, 2002, <code>/foo/bar.log</code> will
be copied to <code>/foo/bar.log.2002-03-08</code>. Logging for the
9th day of March will be output to <code>/foo/bar.log</code> until
it is rolled over the next day.</td></tr>
<tr>
<td><code>'.'yyyy-MM-dd-a</code>
<td>Rollover at midnight and midday of each day.</td>
<td>At noon, on March 9th, 2002, <code>/foo/bar.log</code> will be
copied to <code>/foo/bar.log.2002-03-09-AM</code>. Logging for the
afternoon of the 9th will be output to <code>/foo/bar.log</code>
until it is rolled over at midnight.</td></tr>
<tr>
<td><code>'.'yyyy-MM-dd-HH</code>
<td>Rollover at the top of every hour.</td>
<td>At approximately 11:00.000 o'clock on March 9th, 2002,
<code>/foo/bar.log</code> will be copied to
<code>/foo/bar.log.2002-03-09-10</code>. Logging for the 11th hour
of the 9th of March will be output to <code>/foo/bar.log</code>
until it is rolled over at the beginning of the next hour.</td></tr>
<tr>
<td><code>'.'yyyy-MM-dd-HH-mm</code></td>
<td>Rollover at the beginning of every minute.</td>
<td>At approximately 11:23,000, on March 9th, 2001,
<code>/foo/bar.log</code> will be copied to
<code>/foo/bar.log.2001-03-09-10-22</code>. Logging for the minute
of 11:23 (9th of March) will be output to
<code>/foo/bar.log</code> until it is rolled over the next minute.</td></tr>
</table>
<p>Do not use the colon ":" character in anywhere in the
<b>DatePattern</b> option. The text before the colon is interpeted
as the protocol specificaion of a URL which is probably not what
you want.</p> */
public class ChukwaDailyRollingFileAppender extends FileAppender {
static Logger log = Logger.getLogger(ChukwaDailyRollingFileAppender.class);
// The code assumes that the following constants are in a increasing
// sequence.
static final int TOP_OF_TROUBLE = -1;
static final int TOP_OF_MINUTE = 0;
static final int TOP_OF_HOUR = 1;
static final int HALF_DAY = 2;
static final int TOP_OF_DAY = 3;
static final int TOP_OF_WEEK = 4;
static final int TOP_OF_MONTH = 5;
static final String adaptorType = ChukwaAgentController.CharFileTailUTF8NewLineEscaped;
static final Object lock = new Object();
static String lastRotation = "";
/**
* The date pattern. By default, the pattern is set to "'.'yyyy-MM-dd" meaning
* daily rollover.
*/
private String datePattern = "'.'yyyy-MM-dd";
/**
* The log file will be renamed to the value of the scheduledFilename variable
* when the next interval is entered. For example, if the rollover period is
* one hour, the log file will be renamed to the value of "scheduledFilename"
* at the beginning of the next hour.
*
* The precise time when a rollover occurs depends on logging activity.
*/
private String scheduledFilename;
/**
* The next time we estimate a rollover should occur.
*/
private long nextCheck = System.currentTimeMillis() - 1;
/**
* Regex to select log files to be deleted
*/
private String cleanUpRegex = null;
/**
* Set the maximum number of backup files to keep around.
*/
private int maxBackupIndex = 10;
private ClientFinalizer clientFinalizer = null;
boolean hasBeenActivated = false;
Date now = new Date();
SimpleDateFormat sdf;
RollingCalendar rc = new RollingCalendar();
int checkPeriod = TOP_OF_TROUBLE;
ChukwaAgentController chukwaClient;
boolean chukwaClientIsNull = true;
static final Object chukwaLock = new Object();
String chukwaClientHostname;
int chukwaClientPortNum;
long chukwaClientConnectNumRetry;
long chukwaClientConnectRetryInterval;
String recordType;
// The gmtTimeZone is used only in computeCheckPeriod() method.
static final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
/**
* The default constructor does nothing.
* @throws IOException if constructor initialization error
*/
public ChukwaDailyRollingFileAppender() throws IOException {
super();
}
/**
Instantiate a <code>DailyRollingFileAppender</code> and open the
file designated by <code>filename</code>. The opened filename will
become the output destination for this appender.
* @param layout is logging layout
* @param filename is the filename to write logs
* @param datePattern is the date pattern of log suffix
* @throws IOException if constructor initialization error
*/
public ChukwaDailyRollingFileAppender(Layout layout, String filename,
String datePattern) throws IOException {
super(layout, filename, true);
System.out
.println("Daily Rolling File Appender successfully registered file with agent: "
+ filename);
this.datePattern = datePattern;
}
/**
* The <b>DatePattern</b> takes a string in the same format as expected by
* {@link SimpleDateFormat}. This options determines the rollover schedule.
* @param pattern is date formatting pattern
*/
public void setDatePattern(String pattern) {
datePattern = pattern;
}
/** Returns the value of the <b>DatePattern</b> option.
* @return date pattern
*/
public String getDatePattern() {
return datePattern;
}
public String getRecordType() {
if (recordType != null)
return recordType;
else
return "unknown";
}
public void setRecordType(String recordType) {
this.recordType = recordType;
}
public void activateOptions() {
// Prevent early initialisation
if (!hasBeenActivated)
{ return;}
super.activateOptions();
if (datePattern != null && fileName != null) {
now.setTime(System.currentTimeMillis());
sdf = new SimpleDateFormat(datePattern);
int type = computeCheckPeriod();
printPeriodicity(type);
rc.setType(type);
File file = new File(fileName);
scheduledFilename = fileName + sdf.format(new Date(file.lastModified()));
} else {
LogLog
.error("Either File or DatePattern options are not set for appender ["
+ name + "].");
}
}
void printPeriodicity(int type) {
switch (type) {
case TOP_OF_MINUTE:
LogLog.debug("Appender [" + name + "] to be rolled every minute.");
break;
case TOP_OF_HOUR:
LogLog
.debug("Appender [" + name + "] to be rolled on top of every hour.");
break;
case HALF_DAY:
LogLog.debug("Appender [" + name
+ "] to be rolled at midday and midnight.");
break;
case TOP_OF_DAY:
LogLog.debug("Appender [" + name + "] to be rolled at midnight.");
break;
case TOP_OF_WEEK:
LogLog.debug("Appender [" + name + "] to be rolled at start of week.");
break;
case TOP_OF_MONTH:
LogLog.debug("Appender [" + name
+ "] to be rolled at start of every month.");
break;
default:
LogLog.warn("Unknown periodicity for appender [" + name + "].");
}
}
// This method computes the roll over period by looping over the
// periods, starting with the shortest, and stopping when the r0 is
// different from from r1, where r0 is the epoch formatted according
// the datePattern (supplied by the user) and r1 is the
// epoch+nextMillis(i) formatted according to datePattern. All date
// formatting is done in GMT and not local format because the test
// logic is based on comparisons relative to 1970-01-01 00:00:00
// GMT (the epoch).
int computeCheckPeriod() {
RollingCalendar rollingCalendar = new RollingCalendar(gmtTimeZone,
Locale.ENGLISH);
// set sate to 1970-01-01 00:00:00 GMT
Date epoch = new Date(0);
if (datePattern != null) {
for (int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
simpleDateFormat.setTimeZone(gmtTimeZone); // do all date formatting in
// GMT
String r0 = simpleDateFormat.format(epoch);
rollingCalendar.setType(i);
Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
String r1 = simpleDateFormat.format(next);
// System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);
if (r0 != null && r1 != null && !r0.equals(r1)) {
return i;
}
}
}
return TOP_OF_TROUBLE; // Deliberately head for trouble...
}
/**
* Rollover the current file to a new file.
*/
void rollOver() throws IOException {
/* Compute filename, but only if datePattern is specified */
if (datePattern == null) {
errorHandler.error("Missing DatePattern option in rollOver().");
return;
}
String datedFilename = fileName + sdf.format(now);
// It is too early to roll over because we are still within the
// bounds of the current interval. Rollover will occur once the
// next interval is reached.
if (scheduledFilename.equals(datedFilename)) {
return;
}
// close current file, and rename it to datedFilename
this.closeFile();
File target = new File(scheduledFilename);
if (target.exists()) {
if(!target.delete()) {
LogLog.warn("Unable to remove: "+target.getAbsolutePath());
};
}
File file = new File(fileName);
boolean result = file.renameTo(target);
if (result) {
LogLog.debug(fileName + " -> " + scheduledFilename);
} else {
LogLog.error("Failed to rename [" + fileName + "] to ["
+ scheduledFilename + "].");
}
try {
// This will also close the file. This is OK since multiple
// close operations are safe.
this.setFile(fileName, false, this.bufferedIO, this.bufferSize);
} catch (IOException e) {
errorHandler.error("setFile(" + fileName + ", false) call failed.");
}
scheduledFilename = datedFilename;
cleanUp();
}
public synchronized String getCleanUpRegex() {
return cleanUpRegex;
}
protected synchronized void setCleanUpRegex(String cleanUpRegex) {
this.cleanUpRegex = cleanUpRegex;
}
public int getMaxBackupIndex() {
return maxBackupIndex;
}
public void setMaxBackupIndex(int maxBackupIndex) {
this.maxBackupIndex = maxBackupIndex;
}
protected synchronized void cleanUp() {
String regex = "";
try {
File actualFile = new File(fileName);
String directoryName = actualFile.getParent();
String actualFileName = actualFile.getName();
File dirList = new File(directoryName);
if (cleanUpRegex == null || !cleanUpRegex.contains("$fileName")) {
LogLog
.error("cleanUpRegex == null || !cleanUpRegex.contains(\"$fileName\")");
return;
}
regex = cleanUpRegex.replace("$fileName", actualFileName);
String[] dirFiles = dirList.list(new LogFilter(actualFileName, regex));
List<String> files = new ArrayList<String>();
if(dirFiles!=null) {
for (String file : dirFiles) {
files.add(file);
}
}
Collections.sort(files);
while (files.size() > maxBackupIndex) {
String file = files.remove(0);
File f = new File(directoryName + "/" + file);
if(!f.delete()) {
LogLog.warn("Cannot remove: " + file);
}
}
} catch (Exception e) {
errorHandler
.error("cleanUp(" + fileName + "," + regex + ") call failed.");
}
}
private static class LogFilter implements FilenameFilter {
private Pattern p = null;
private String logFile = null;
public LogFilter(String logFile, String regex) {
this.logFile = logFile;
p = Pattern.compile(regex);
}
@Override
public boolean accept(File dir, String name) {
// ignore current log file
if (name.intern() == this.logFile.intern()) {
return false;
}
// ignore file without the same prefix
if (!name.startsWith(logFile)) {
return false;
}
return p.matcher(name).find();
}
}
/**
* Fix for late-initialisation
*/
@Override
protected boolean checkEntryConditions() {
synchronized(chukwaLock) {
if (!hasBeenActivated) {
hasBeenActivated = true;
activateOptions();
}
}
return super.checkEntryConditions();
}
/**
* This method differentiates DailyRollingFileAppender from its super class.
*
* <p>Before actually logging, this method will check whether it is time to do
* a rollover. If it is, it will schedule the next rollover time and then
* rollover.
* */
protected void subAppend(LoggingEvent event) {
try {
// we set up the chukwa adaptor here because this is the first
// point which is called after all setters have been called with
// their values from the log4j.properties file, in particular we
// needed to give setCukwaClientPortNum() and -Hostname() a shot
// Make sure only one thread can do this
// and use the boolean to avoid the first level locking
if (chukwaClientIsNull) {
synchronized (chukwaLock) {
String log4jFileName = getFile();
String recordType = getRecordType();
long currentLength = 0L;
try {
File fooLog = new File(log4jFileName);
log4jFileName = fooLog.getAbsolutePath();
currentLength = fooLog.length();
} catch (Throwable e) {
log.warn("Exception while trying to get current file size for " + log4jFileName);
currentLength = 0L;
}
if (chukwaClient == null) {
if (getChukwaClientHostname() != null
&& getChukwaClientPortNum() != 0) {
chukwaClient = new ChukwaAgentController(
getChukwaClientHostname(), getChukwaClientPortNum());
log.debug("setup adaptor with hostname "
+ getChukwaClientHostname() + " and portnum "
+ getChukwaClientPortNum());
} else {
chukwaClient = new ChukwaAgentController();
log
.debug("setup adaptor with no args, which means it used its defaults");
}
chukwaClientIsNull = false;
// Watchdog is watching for ChukwaAgent only once every 5 minutes,
// so there's no point in retrying more than once every 5 mins.
// In practice, if the watchdog is not able to automatically restart
// the agent, it will take more than 20 minutes to get Ops to
// restart it.
// Also its a good to limit the number of communications between
// Hadoop and Chukwa, that's why 30 minutes.
long retryInterval = chukwaClientConnectRetryInterval;
if (retryInterval == 0) {
retryInterval = 1000 * 60 * 30;
}
long numRetries = chukwaClientConnectNumRetry;
if (numRetries == 0) {
numRetries = 48;
}
String name = AdaptorNamingUtils.synthesizeAdaptorID
(ChukwaAgentController.CharFileTailUTF8NewLineEscaped, recordType, log4jFileName);
String adaptorID = chukwaClient.addByName(name, ChukwaAgentController.CharFileTailUTF8NewLineEscaped,
recordType,currentLength + " " + log4jFileName, currentLength,
numRetries, retryInterval);
// Setup a shutdownHook for the controller
clientFinalizer = new ClientFinalizer(chukwaClient);
Runtime.getRuntime().addShutdownHook(clientFinalizer);
if (adaptorID != null) {
log.debug("Added file tailing adaptor to chukwa agent for file "
+ log4jFileName + ", adaptorId:" + adaptorID
+ " using this recordType :" + recordType
+ ", starting at offset:" + currentLength);
} else {
log.debug("Chukwa adaptor not added, addFile(" + log4jFileName
+ ") returned, current offset: " + currentLength);
}
}
}
}
long n = System.currentTimeMillis();
if (n >= nextCheck) {
now.setTime(n);
nextCheck = rc.getNextCheckMillis(now);
try {
rollOver();
} catch (IOException ioe) {
LogLog.error("rollOver() failed.", ioe);
}
}
boolean written = false;
if(layout.ignoresThrowable()) {
String[] s = event.getThrowableStrRep();
if (s != null) {
int len = s.length;
StringBuilder sb = new StringBuilder();
sb.append(this.layout.format(event));
for(int i = 0; i < len; i++) {
sb.append(s[i]).append("\n");
}
//escape the newlines from record bodies, exception and then write this record to the log file
written = true;
this.qw.write(RecordConstants.escapeAllButLastRecordSeparator("\n",sb.toString()));
}
}
if (!written) {
//escape the newlines from record bodies and then write this record to the log file
this.qw.write(RecordConstants.escapeAllButLastRecordSeparator("\n",this.layout.format(event)));
}
if (this.immediateFlush) {
this.qw.flush();
}
} catch (Throwable e) {
System.err.println("Exception in ChukwaRollingAppender: "
+ e.getMessage());
e.printStackTrace();
}
}
public String getChukwaClientHostname() {
return chukwaClientHostname;
}
public void setChukwaClientHostname(String chukwaClientHostname) {
this.chukwaClientHostname = chukwaClientHostname;
}
public int getChukwaClientPortNum() {
return chukwaClientPortNum;
}
public void setChukwaClientPortNum(int chukwaClientPortNum) {
this.chukwaClientPortNum = chukwaClientPortNum;
}
public void setChukwaClientConnectNumRetry(int i) {
this.chukwaClientConnectNumRetry = i;
}
public void setChukwaClientConnectRetryInterval(long i) {
this.chukwaClientConnectRetryInterval = i;
}
}
/**
* RollingCalendar is a helper class to DailyRollingFileAppender. Given a
* periodicity type and the current time, it computes the start of the next
* interval.
* */
class RollingCalendar extends GregorianCalendar {
/**
*
*/
private static final long serialVersionUID = 2153481574198792767L;
int type = ChukwaDailyRollingFileAppender.TOP_OF_TROUBLE;
RollingCalendar() {
super();
}
RollingCalendar(TimeZone tz, Locale locale) {
super(tz, locale);
}
void setType(int type) {
this.type = type;
}
public long getNextCheckMillis(Date now) {
return getNextCheckDate(now).getTime();
}
public Date getNextCheckDate(Date now) {
this.setTime(now);
switch (type) {
case ChukwaDailyRollingFileAppender.TOP_OF_MINUTE:
this.set(Calendar.SECOND, 0);
this.set(Calendar.MILLISECOND, 0);
this.add(Calendar.MINUTE, 1);
break;
case ChukwaDailyRollingFileAppender.TOP_OF_HOUR:
this.set(Calendar.MINUTE, 0);
this.set(Calendar.SECOND, 0);
this.set(Calendar.MILLISECOND, 0);
this.add(Calendar.HOUR_OF_DAY, 1);
break;
case ChukwaDailyRollingFileAppender.HALF_DAY:
this.set(Calendar.MINUTE, 0);
this.set(Calendar.SECOND, 0);
this.set(Calendar.MILLISECOND, 0);
int hour = get(Calendar.HOUR_OF_DAY);
if (hour < 12) {
this.set(Calendar.HOUR_OF_DAY, 12);
} else {
this.set(Calendar.HOUR_OF_DAY, 0);
this.add(Calendar.DAY_OF_MONTH, 1);
}
break;
case ChukwaDailyRollingFileAppender.TOP_OF_DAY:
this.set(Calendar.HOUR_OF_DAY, 0);
this.set(Calendar.MINUTE, 0);
this.set(Calendar.SECOND, 0);
this.set(Calendar.MILLISECOND, 0);
this.add(Calendar.DATE, 1);
break;
case ChukwaDailyRollingFileAppender.TOP_OF_WEEK:
this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek());
this.set(Calendar.HOUR_OF_DAY, 0);
this.set(Calendar.SECOND, 0);
this.set(Calendar.MILLISECOND, 0);
this.add(Calendar.WEEK_OF_YEAR, 1);
break;
case ChukwaDailyRollingFileAppender.TOP_OF_MONTH:
this.set(Calendar.DATE, 1);
this.set(Calendar.HOUR_OF_DAY, 0);
this.set(Calendar.SECOND, 0);
this.set(Calendar.MILLISECOND, 0);
this.add(Calendar.MONTH, 1);
break;
default:
throw new IllegalStateException("Unknown periodicity type.");
}
return getTime();
}
@Override
public boolean equals(Object o) {
return super.equals(o);
}
@Override
public int hashCode() {
return super.hashCode();
}
}