/* * 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. */ package tachyon; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.Writer; import java.lang.Math; import java.net.InetAddress; import java.net.UnknownHostException; import org.apache.log4j.FileAppender; import org.apache.log4j.spi.ErrorCode; import org.apache.log4j.spi.LoggingEvent; import tachyon.util.CommonUtils; /** * Custom log4j appender which preserves old logs on system restart, rolls over logs based on both * size and day. Also implements batch deletion of logs when the maximum backup index is reached. */ public class Log4jFileAppender extends FileAppender { private int mMaxBackupIndex = 1; private int mMaxFileSizeBytes = Constants.MB; private int mCurrentFileBackupIndex = -1; private int mDeletionPercentage = 10; private String mCurrentFileName = ""; private String mOriginalFileName = ""; private String mLastDate = ""; /** * Called when a new log attempt is made, either due to server restart or rollover. The filename * is modified to identify the logging node in getNewFileName. */ @Override public void activateOptions() { if (fileName != null) { if (!fileName.equals(mCurrentFileName)) { mOriginalFileName = fileName; } else { fileName = mOriginalFileName; } try { fileName = getNewLogFileName(fileName); setFile(fileName, fileAppend, bufferedIO, bufferSize); } catch (Exception e) { errorHandler.error("Error while activating log options", e, ErrorCode.FILE_OPEN_FAILURE); } } } /** * Gets a log file name which includes the logger's host address and the date. * * @param fileName * The base filename * @return A new filename string */ private String getNewLogFileName(String fileName) { if (!fileName.isEmpty()) { String newFileName = ""; String address = ""; try { address = "@" + InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException uhe) { address = "@UnknownHost"; } newFileName = fileName + address + "_" + CommonUtils.convertMsToSimpleDate(System.currentTimeMillis()); File file = new File(newFileName); if (file.exists()) { rotateLogs(newFileName); } mLastDate = CommonUtils.convertMsToSimpleDate(System.currentTimeMillis()); mCurrentFileName = newFileName; return newFileName; } else { throw new RuntimeException("Log4j configuration error, null filepath"); } } /** * Rotates logs. The previous current log is set to the next available index. If the index has * reached the maximum backup index, a percent of backup logs will be deleted, started from the * earliest first. Then all rolledover logs will be moved up. * * @param fileName * The fileName of the new current log. */ private void rotateLogs(String fileName) { if (mCurrentFileBackupIndex == -1) { int lo = 0; int hi = mMaxBackupIndex; while (lo <= hi) { int mid = lo + (hi - lo) / 2; if (mid == 0) { mCurrentFileBackupIndex = 1; break; } if (new File(fileName + "_" + mid).exists()) { if (new File(fileName + "_" + (mid + 1)).exists()) { lo = mid; } else { mCurrentFileBackupIndex = mid + 1; break; } } else { if (new File(fileName + "_" + (mid - 1)).exists()) { mCurrentFileBackupIndex = mid; break; } else { hi = mid; } } } } File oldFile = new File(fileName); if (mCurrentFileBackupIndex >= mMaxBackupIndex) { int deleteToIndex = (int) Math.ceil(mMaxBackupIndex * mDeletionPercentage / 100.0); for (int i = 1; i < deleteToIndex; i ++) { new File(fileName + "_" + i).delete(); } for (int i = deleteToIndex + 1; i <= mMaxBackupIndex; i ++) { new File(fileName + "_" + i).renameTo(new File(fileName + "_" + (i - deleteToIndex))); } mCurrentFileBackupIndex = mCurrentFileBackupIndex - deleteToIndex; } oldFile.renameTo(new File(fileName + "_" + mCurrentFileBackupIndex)); mCurrentFileBackupIndex ++; } public void setDeletionPercentage(int deletionPercentage) { if (deletionPercentage > 0 && deletionPercentage <= 100) { mDeletionPercentage = deletionPercentage; } else { throw new RuntimeException("Log4j configuration error, invalid deletionPercentage"); } } /** * Creates a LazyFileOutputStream so logs are only created when a message is logged. * * @param fileName * @param append * @param bufferedIO * @param bufferSize */ @Override public synchronized void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize) throws IOException { // It does not make sense to have immediate flush and bufferedIO. if (bufferedIO) { setImmediateFlush(false); } reset(); // Creation of the LazyFileOutputStream object (the responsible of the log writing operations) LazyFileOutputStream ostream = new LazyFileOutputStream(fileName, append); Writer fw = createWriter(ostream); if (bufferedIO) { fw = new BufferedWriter(fw, bufferSize); } setQWForFiles(fw); this.fileName = fileName; this.fileAppend = append; this.bufferedIO = bufferedIO; this.bufferSize = bufferSize; writeHeader(); } public void setMaxBackupIndex(int maxBackups) { mMaxBackupIndex = maxBackups; } public void setMaxFileSize(int maxFileSizeMB) { mMaxFileSizeBytes = maxFileSizeMB * Constants.MB; } /** * Called whenever a new message is logged. Checks both the date and size to determine if rollover * is necessary. * * @param event */ @Override public synchronized void subAppend(LoggingEvent event) { File currentLog = new File(mCurrentFileName); if (currentLog.length() > mMaxFileSizeBytes || !CommonUtils.convertMsToSimpleDate(System.currentTimeMillis()).equals(mLastDate)) { activateOptions(); } if (currentLog.exists()) { super.subAppend(event); } else { String parentName = currentLog.getParent(); if (parentName != null) { File parent = new File(parentName); if (parent.exists()) { super.subAppend(event); } else { if (parent.mkdirs()) { super.subAppend(event); } } } } } }