// --- BEGIN COPYRIGHT BLOCK ---
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; version 2 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// (C) 2007 Red Hat, Inc.
// All rights reserved.
// --- END COPYRIGHT BLOCK ---
package com.netscape.cms.logging;
import java.io.*;
import java.util.*;
import java.text.*;
import javax.servlet.*;
import javax.servlet.http.*;
import com.netscape.certsrv.apps.*;
import com.netscape.certsrv.base.*;
import com.netscape.certsrv.common.*;
import com.netscape.certsrv.logging.*;
import com.netscape.cmsutil.util.*;
/**
* A rotating log file for Certificate log events. This class loosely follows
* the Netscape Common Log API implementing rollover interval, size and file
* naming conventions. It does not yet implement Disk Usage.
*
* @version $Revision: 14561 $, $Date: 2007-05-01 10:28:56 -0700 (Tue, 01 May 2007) $
*/
public class RollingLogFile extends LogFile {
public static final String PROP_MAX_FILE_SIZE = "maxFileSize";
public static final String PROP_ROLLOVER_INTERVAL = "rolloverInterval";
public static final String PROP_EXPIRATION_TIME = "expirationTime";
/**
* The default max file size in bytes
*/
static final int MAX_FILE_SIZE = 100;
/**
* The default rollover interval in seconds
*/
static final String ROLLOVER_INTERVAL = "2592000";
/**
* The default expiration time in seconds
*/
static final String EXPIRATION_TIME = "2592000";
/**
* The maximum file size in bytes
*/
protected int mMaxFileSize = 0;
/**
* The amount of time in miniseconds between log rotations
*/
protected long mRolloverInterval = 0;
/**
* The thread responsible for rotating the log
*/
private Thread mRolloverThread = null;
/**
* The incrementing backup number for the log file names
*/
private int mFileNumber = 1;
/**
* The amount of time before a backed up log is removed in milliseconds
*/
protected long mExpirationTime = 0;
/**
* The thread responsible for removing expired log files
*/
private Thread mExpirationThread = null;
/**
* The object used as a lock for expiration thread synchronization
*/
private Object mExpLock = new Object();
private final static String LOGGING_SIGNED_AUDIT_LOG_DELETE =
"LOGGING_SIGNED_AUDIT_LOG_DELETE_3";
/**
* Construct a RollingLogFile
*/
public RollingLogFile() {
}
/**
* Initialize and open a RollingLogFile using the prop config store
*
* @param config The property config store to find values in
*/
public void init(IConfigStore config) throws IOException,
EBaseException {
super.init(config);
rl_init(config.getInteger(PROP_MAX_FILE_SIZE, MAX_FILE_SIZE),
config.getString(PROP_ROLLOVER_INTERVAL, ROLLOVER_INTERVAL),
config.getString(PROP_EXPIRATION_TIME, EXPIRATION_TIME));
}
/**
* Convenience routine to initialized the RollingLogFile specific
* attributes.
*/
protected void rl_init(int maxFileSize, String rolloverInterval,
String expirationTime) {
mMaxFileSize = maxFileSize * 1024;
setRolloverTime(rolloverInterval);
setExpirationTime(expirationTime);
}
public void startup() throws EBaseException {
super.startup();
}
/**
* Shutdown this log file.
*/
public synchronized void shutdown() {
setRolloverTime("0");
setExpirationTime("0");
super.shutdown();
}
/**
* Set the rollover interval
*
* @param rolloverSeconds The amount of time in seconds until the log
* is rotated. A value of 0 will disable log rollover.
**/
public synchronized void setRolloverTime(String rolloverSeconds) {
mRolloverInterval = Long.valueOf(rolloverSeconds).longValue() * 1000;
if ((mRolloverThread == null) && (mRolloverInterval > 0)) {
mRolloverThread = new RolloverThread();
mRolloverThread.setDaemon(true);
mRolloverThread.start();
}
this.notify();
}
/**
* Get the rollover interval
*
* @return The interval in seconds in which the log is rotated
**/
public synchronized int getRolloverTime() {
return (int) (mRolloverInterval / 1000);
}
/**
* Set the file expiration time
*
* @param expirationSeconds The amount of time in seconds until log files
* are deleted
**/
public void setExpirationTime(String expirationSeconds) {
// Need to completely protect changes to mExpiration time
// and make sure they only happen while the thread is sleeping
synchronized (mExpLock) {
mExpirationTime = Long.valueOf(expirationSeconds).longValue() * 1000;
if (mExpirationThread == null) {
if (mExpirationTime > 0) {
mExpirationThread = new ExpirationThread();
mExpirationThread.setDaemon(true);
mExpirationThread.start();
}
} else {
mExpLock.notify();
}
}
}
/**
* Get the expiration time
*
* @return The age in seconds in which log files are delete
**/
public int getExpirationTime() {
return (int) (mExpirationTime / 1000);
}
/**
* Rotate the log file to a backup file with a incrementing integer
* extension
**/
public synchronized void rotate()
throws IOException {
//File backupFile = new File(mFileName + "." + mFileNumber);
File backupFile = new File(mFileName + "." + mLogFileDateFormat.format(mDate));
// close, backup, and reopen the log file zeroizing its contents
super.close();
try {
if( Utils.isNT() ) {
// NT is very picky on the path
Utils.exec( "copy " +
mFile.getCanonicalPath().replace( '/', '\\' ) +
" " +
backupFile.getCanonicalPath().replace( '/',
'\\' ) );
} else {
// Create a copy of the original file which
// preserves the original file permissions.
Utils.exec( "cp -p " + mFile.getCanonicalPath() + " " +
backupFile.getCanonicalPath() );
}
// Zeroize the original file if and only if
// the backup copy was successful.
if( backupFile.exists() ) {
// Make certain that the backup file has
// the correct permissions.
if( !Utils.isNT() ) {
Utils.exec( "chmod 00640 " + backupFile.getCanonicalPath() );
}
try {
// Open and close the original file
// to zeroize its contents.
PrintWriter pw = new PrintWriter( mFile );
pw.close();
// Make certain that the original file retains
// the correct permissions.
if( !Utils.isNT() ) {
Utils.exec( "chmod 00640 " + mFile.getCanonicalPath() );
}
} catch ( FileNotFoundException e ) {
CMS.debug( "Unable to zeroize "
+ mFile.toString() );
}
} else {
CMS.debug( "Unable to backup "
+ mFile.toString() + " to "
+ backupFile.toString() );
}
} catch( Exception e ) {
CMS.debug( "Unable to backup "
+ mFile.toString() + " to "
+ backupFile.toString() );
}
super.open(); // will reset mBytesWritten
mFileNumber++;
}
/**
* Remove any log files which have not been modified in the specified
* time
*
*
* NOTE: automatic removal of log files is currently NOT supported!
*
*
*
* - signed.audit LOGGING_SIGNED_AUDIT_LOG_DELETE used AFTER audit log
* expires (authorization should not allow, but in case authorization gets
* compromised make sure it is written AFTER the log expiration happens)
*
* @param expirationSeconds The number of seconds since the expired files
* have been modified.
* @return the time in milliseconds when the next file expires
**/
public long expire(long expirationSeconds) throws ELogException {
String auditMessage = null;
if (expirationSeconds <= 0)
throw new ELogException(CMS.getUserMessage("CMS_LOG_EXPIRATION_TIME_ZERO"));
long expirationTime = expirationSeconds * 1000;
long currentTime = System.currentTimeMillis();
long oldestFile = currentTime;
String dirName = mFile.getParent();
if (dirName == null)
dirName = ".";
File dir = new File(dirName);
// Get just the base name, minus the .date extension
//int len = mFile.getName().length() - LogFile.DATE_PATTERN.length() - 1;
//String baseName = mFile.getName().substring(0, len);
String fileName = mFile.getName();
String baseName = null, pathName = null;
int index = fileName.lastIndexOf("/");
if (index != -1) { // "/" exist in fileName
pathName = fileName.substring(0, index);
baseName = fileName.substring(index + 1);
if (dirName == null) {
dirName = pathName;
} else {
dirName = dirName.concat("/" + pathName);
}
}else { // "/" NOT exist in fileName
baseName = fileName;
}
fileFilter ff = new fileFilter(baseName + ".");
String[] filelist = dir.list(ff);
if (filelist == null) { // Crap! Something is wrong.
throw new
ELogException(CMS.getUserMessage("CMS_LOG_DIRECTORY_LIST_FAILED",
dirName, ff.toString()));
}
// Walk through the list of files which match this log file name
// and delete the old ones.
for (int i = 0; i < filelist.length; i++) {
if (pathName != null) {
filelist[i] = pathName + "/" + filelist[i];
}else {
filelist[i] = dirName + "/" + filelist[i];
}
String fullname = dirName + File.separatorChar + filelist[i];
File file = new File(fullname);
long fileTime = file.lastModified();
// Java documentation on File says lastModified() should not
// be interpeted. The doc is wrong. See JavaSoft bug #4094538
if ((currentTime - fileTime) > expirationTime) {
file.delete();
if (file.exists()) {
// log failure in deleting an expired signed audit log file
auditMessage = CMS.getLogMessage(
LOGGING_SIGNED_AUDIT_LOG_DELETE,
ILogger.SYSTEM_UID,
ILogger.FAILURE,
fullname);
} else {
// log success in deleting an expired signed audit log file
auditMessage = CMS.getLogMessage(
LOGGING_SIGNED_AUDIT_LOG_DELETE,
ILogger.SYSTEM_UID,
ILogger.SUCCESS,
fullname);
}
audit(auditMessage);
} else if (fileTime < oldestFile) {
oldestFile = fileTime;
}
}
return oldestFile + expirationTime;
}
//
// Rollover and Expiration threads
//
// At first glance you may think it's a waste of thread resources to have
// two threads for every log file, but the truth is that these threads are
// sleeping 99% of the time. NxN thread implementations (Solaris, NT,
// IRIX 6.4, Unixware, etc...) will handle these in user space.
//
// You may be able to join these into one thread, and deal with
// multiple wakeup times, but the code would sure look ugly, and the race
// conditions are numerous as is. Furthermore, this is what user space
// threads will do for you anyways.
//
/**
* Log rotation thread. Sleep for the rollover interval and rotate the
* log. Changing rollover interval to 0 will cause this thread to exit.
*/
final class RolloverThread extends Thread {
/**
* Rollover thread constructor including thread name
*/
public RolloverThread() {
super();
super.setName(mFileName + ".rollover-" + (Thread.activeCount() + 1));
}
public void run() {
while (mRolloverInterval > 0) {
// Sleep for the interval and then rotate the log
synchronized (RollingLogFile.this) {
try {
RollingLogFile.this.wait(mRolloverInterval);
} catch (InterruptedException e) {
// This shouldn't happen very often
CMS.getLogger().getLogQueue().log(new
SystemEvent(CMS.getUserMessage("CMS_LOG_THREAD_INTERRUPT", "rollover")));
}
}
if (mRolloverInterval == 0) {
break;
}
if (mBytesWritten > 0) {
try {
rotate();
} catch (IOException e) {
ConsoleError.send(new
SystemEvent(CMS.getUserMessage("CMS_LOG_ROTATE_LOG_FAILED", mFile.getName(), e.toString())));
break;
}
}
// else
// Don't rotate empty logs
// flag in log summary file?
}
mRolloverThread = null;
}
}
/**
* Log expiration thread. Sleep for the expiration interval and
* delete any files which are too old.
* Changing expiration interval to 0 will cause this thread to exit.
*/
final class ExpirationThread extends Thread {
/**
* ExpirationThread thread constructor including thread name
*/
public ExpirationThread() {
super();
super.setName(mFileName + ".expiration-" + (Thread.activeCount() + 1));
}
public void run() {
synchronized (mExpLock) {
while (mExpirationTime > 0) {
long wakeupTime = 0;
long sleepTime = 0;
// First, remove any old log files and figure out when the
// next one expires
try {
wakeupTime = expire((long) (mExpirationTime / 1000));
} catch (SecurityException e) {
ConsoleError.send(new
SystemEvent(CMS.getUserMessage("CMS_LOG_EXPIRE_LOG_FAILED", e.toString())));
break;
} catch (ELogException e) {
ConsoleError.send(new
SystemEvent(CMS.getUserMessage("CMS_LOG_EXPIRE_LOG_FAILED", e.toString())));
break;
}
sleepTime = wakeupTime - System.currentTimeMillis();
//System.out.println("wakeup " + wakeupTime);
//System.out.println("current "+System.currentTimeMillis());
//System.out.println("sleep " + sleepTime);
// Sleep for the interval and then check the directory
// Note: mExpirationTime can only change while we're
// sleeping
if (sleepTime > 0) {
try {
mExpLock.wait(sleepTime);
} catch (InterruptedException e) {
// This shouldn't happen very often
ConsoleError.send(new
SystemEvent(CMS.getUserMessage("CMS_LOG_THREAD_INTERRUPT", "expiration")));
}
}
}
}
mExpirationThread = null;
}
}
/**
* Write an event to the log file
*
* @param ev The event to be logged.
**/
public synchronized void log(ILogEvent ev) throws ELogException {
//xxx, Shall we log first without checking if it exceed the maximum?
super.log(ev); // Will increment mBytesWritten
if ((0 != mMaxFileSize) && (mBytesWritten > mMaxFileSize)) {
flush();
try {
rotate();
} catch (IOException e) {
throw new ELogException(CMS.getUserMessage("CMS_LOG_ROTATE_LOG_FAILED", mFile.getName(), e.toString()));
}
}
}
/**
* Retrieve log file list.
*/
public synchronized NameValuePairs retrieveLogList(Hashtable req,
Hashtable resp) throws ServletException,
IOException, EBaseException {
NameValuePairs params = new NameValuePairs();
String[] files = null;
files = fileList();
for (int i = 0; i < files.length; i++) {
params.add(files[i], "");
}
return params;
}
/**
* Get the log file list in the log directory
*
* @return an array of filenames with related path to cert server root
*/
protected String[] fileList() {
String pathName = null, baseName = null;
String dirName = mFile.getParent();
String fileName = mFile.getName();
int index = fileName.lastIndexOf("/");
if (index != -1) { // "/" exist in fileName
pathName = fileName.substring(0, index);
baseName = fileName.substring(index + 1);
if (dirName == null) {
dirName = pathName;
} else {
dirName = dirName.concat("/" + pathName);
}
}else { // "/" NOT exist in fileName
baseName = fileName;
}
File dir = new File(dirName);
fileFilter ff = new fileFilter(baseName + ".");
//There are some difference here. both should work
//error,logs,logs/error jdk115
//logs/system,., logs/system jdk116
//System.out.println(mFile.getName()+","+dirName+","+mFile.getPath()); //log/system,.
String[] filelist = dir.list(ff);
for (int i = 0; i < filelist.length; i++) {
if (pathName != null) {
filelist[i] = pathName + "/" + filelist[i];
}else {
filelist[i] = dirName + "/" + filelist[i];
}
}
return filelist;
}
public String getImplName() {
return "RollingLogFile";
}
public String getDescription() {
return "RollingLogFile";
}
public Vector getDefaultParams() {
Vector v = super.getDefaultParams();
v.addElement(PROP_MAX_FILE_SIZE + "=");
v.addElement(PROP_ROLLOVER_INTERVAL + "=");
//v.addElement(PROP_EXPIRATION_TIME + "=");
return v;
}
public Vector getInstanceParams() {
Vector v = super.getInstanceParams();
try {
v.addElement(PROP_MAX_FILE_SIZE + "=" + mMaxFileSize / 1024);
if (mRolloverInterval / 1000 <= 60 * 60)
v.addElement(PROP_ROLLOVER_INTERVAL + "=" + "Hourly");
else if (mRolloverInterval / 1000 <= 60 * 60 * 24)
v.addElement(PROP_ROLLOVER_INTERVAL + "=" + "Daily");
else if (mRolloverInterval / 1000 <= 60 * 60 * 24 * 7)
v.addElement(PROP_ROLLOVER_INTERVAL + "=" + "Weekly");
else if (mRolloverInterval / 1000 <= 60 * 60 * 24 * 30)
v.addElement(PROP_ROLLOVER_INTERVAL + "=" + "Monthly");
else if (mRolloverInterval / 1000 <= 60 * 60 * 24 * 366)
v.addElement(PROP_ROLLOVER_INTERVAL + "=" + "Yearly");
//v.addElement(PROP_EXPIRATION_TIME + "=" + mExpirationTime / 1000);
} catch (Exception e) {
}
return v;
}
public String[] getExtendedPluginInfo(Locale locale) {
String[] p = super.getExtendedPluginInfo(locale);
Vector info = new Vector();
for (int i = 0; i < p.length; i++) {
if (!p[i].startsWith(IExtendedPluginInfo.HELP_TOKEN) && !p[i].startsWith(IExtendedPluginInfo.HELP_TEXT))
info.addElement(p[i]);
}
info.addElement(PROP_MAX_FILE_SIZE + ";integer;If the current log file size if bigger than this parameter in kilobytes(KB), the file will be rotated.");
info.addElement(PROP_ROLLOVER_INTERVAL + ";choice(Hourly,Daily,Weekly,Monthly,Yearly);The frequency of the log being rotated.");
info.addElement(PROP_EXPIRATION_TIME + ";integer;The amount of time before a backed up log is removed in seconds");
info.addElement(IExtendedPluginInfo.HELP_TOKEN +
//";configuration-logrules-rollinglogfile");
";configuration-adminbasics");
info.addElement(IExtendedPluginInfo.HELP_TEXT +
";Write the log messages to a file which will be rotated automatically.");
String[] params = new String[info.size()];
info.copyInto(params);
return params;
}
}
/**
* A file filter to select the file with a given prefix
*/
class fileFilter implements FilenameFilter {
String patternToMatch = null;
public fileFilter (String pattern) {
patternToMatch = pattern;
}
public boolean accept(File dir, String name) {
if (name.startsWith(patternToMatch))
return true;
else
return false;
}
}