/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can obtain
* a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
* or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
* Sun designates this particular file as subject to the "Classpath" exception
* as provided by Sun in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the License
* Header, with the fields enclosed by brackets [] replaced by your own
* identifying information: "Portions Copyrighted [year]
* [name of copyright owner]"
*
* Contributor(s):
*
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.sun.enterprise.server.logging;
import java.io.*;
import java.util.Date;
import java.util.LinkedList;
import java.util.ArrayList;
import java.text.MessageFormat;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.FieldPosition;
import java.util.logging.StreamHandler;
import java.util.logging.LogRecord;
import java.util.logging.Level;
import java.util.logging.ErrorManager;
import java.util.logging.Formatter;
import javax.management.ObjectName;
import javax.management.MBeanServer;
import com.sun.enterprise.util.StringUtils;
import com.sun.enterprise.server.ApplicationServer;
import com.sun.enterprise.server.ServerContext;
import com.sun.enterprise.server.logging.stats.ErrorStatistics;
import com.sun.enterprise.server.logging.logviewer.backend.LogFilter;
import com.sun.enterprise.server.logging.logviewer.backend.LogFile;
import com.sun.enterprise.config.serverbeans.LogService;
// Delete
import java.util.HashMap;
import java.util.logging.Logger;
import java.util.Vector;
/**
* FileandSyslogHandler publishes formatted log Messages to a FILE.
*
* @AUTHOR: Hemanth Puttaswamy
* _REVISIT_:
* TODO:
* 1. Ability to lock log files to get exclusive write access
*/
public class FileandSyslogHandler extends StreamHandler {
// This is a OutputStream to keep track of number of bytes
// written out to the stream
private MeteredStream meter;
private final AMXLoggingHook mAMXLoggingHook;
private Date date = new Date( );
private static final String LOGS_DIR = "logs";
private String logFileName = "server.log";
private String absoluteFileName = null;
private static boolean isInitialized = false;
private LogMBean logMBean;
private LogService logService = null;
private static final int WARNING = Level.WARNING.intValue();
private static final int SEVERE = Level.SEVERE.intValue();
private static final String INSTANCE_ROOT_PROPERTY =
"com.sun.aas.instanceRoot";
private static final String LOGGING_MAX_HISTORY_FILES = "com.sun.enterprise.server.logging.max_history_files";
// For now the mimimum rotation value is 0.5 MB.
private static final int MINIMUM_FILE_ROTATION_VALUE = 500000;
// Initially the LogRotation will be off until the domain.xml value is read.
private int limitForFileRotation = 0;
private Object fileUpdateLock = new Object( );
private boolean rotationInProgress = false;
private static final FileandSyslogHandler thisInstance =
new FileandSyslogHandler( );
private LinkedList pendingLogRecordList = new LinkedList();
// Rotation can be done in 3 ways
// 1. Based on the Size: Rotate when some Threshold number of bytes are
// written to server.log
// 2. Based on the Time: Rotate ever 'n' minutes, mostly 24 hrs
// 3. Rotate now
// For mechanisms 2 and 3 we will use this flag. The rotate() will always
// be fired from the publish( ) method for consistency
private static boolean rotationRequested = false;
public static synchronized FileandSyslogHandler getInstance( ) {
return thisInstance;
}
private static final String LOG_ROTATE_DATE_FORMAT =
"yyyy-MM-dd'T'HH-mm-ss";
private static final SimpleDateFormat logRotateDateFormatter =
new SimpleDateFormat( LOG_ROTATE_DATE_FORMAT );
// We maintain a list (Vector) of the last MAX_RECENT_ERRORS WARNING
// or SEVERE error messages that were logged. The DAS (or any other
// client) can then obtain these messages through the ServerRuntimeMBean
// and determine if the server instance (or Node Agent) is in an
// error state.
private static Vector _recentErrors = new Vector();
private static final int MAX_RECENT_ERRORS = 4;
public synchronized static Vector getRecentErrorMessages() {
return _recentErrors;
}
public synchronized static void clearRecentErrorMessages() {
_recentErrors = new Vector();
}
private synchronized static void addRecentErrorMessage(String error) {
if (_recentErrors.size() < MAX_RECENT_ERRORS) {
_recentErrors.add(error);
} else {
_recentErrors.removeElementAt(0);
_recentErrors.add(MAX_RECENT_ERRORS - 1, error);
}
}
/**
* This method is invoked from LogManager.reInitializeLoggers() to
* change the location of the file.
*/
void changeFileName( String fileName ) {
// If the file name is same as the current file name, there
// is no need to change the filename
if( fileName.trim().equals( absoluteFileName ) ) {
return;
}
synchronized( this ) {
super.flush( );
super.close();
try {
openFile( fileName );
// Change the file for LogViewer
LogFilter.setLogFile( new LogFile( fileName ) );
absoluteFileName = fileName;
} catch( IOException ix ) {
new ErrorManager().error(
"FATAL ERROR: COULD NOT OPEN LOG FILE. " +
"Please Check to make sure that the directory for " +
"Logfile exists. Currently reverting back to use the " +
" default server.log", ix, ErrorManager.OPEN_FAILURE );
try {
// Reverting back to the old server.log
openFile( absoluteFileName );
} catch( Exception e ) {
new ErrorManager().error(
"FATAL ERROR: COULD NOT RE-OPEN SERVER LOG FILE. ", e,
ErrorManager.OPEN_FAILURE );
}
}
}
}
/**
* A simple getter to access the complete fileName used by this FileHandler.
*/
String getAbsoluteLogFileName( ) {
return absoluteFileName;
}
/**
* A package private method to set the limit for File Rotation.
*/
synchronized void setLimitForRotation( int rotationLimitInBytes ) {
if ((rotationLimitInBytes == 0) ||
(rotationLimitInBytes >= MINIMUM_FILE_ROTATION_VALUE )) {
limitForFileRotation = rotationLimitInBytes;
}
}
// NOTE: This private class is copied from java.util.logging.FileHandler
// A metered stream is a subclass of OutputStream that
// (a) forwards all its output to a target stream
// (b) keeps track of how many bytes have been written
private class MeteredStream extends OutputStream {
OutputStream out;
long written;
MeteredStream(OutputStream out, long written) {
this.out = out;
this.written = written;
}
public void write(int b) throws IOException {
out.write(b);
written++;
}
public void write(byte buff[]) throws IOException {
out.write(buff);
written += buff.length;
}
public void write(byte buff[], int off, int len) throws IOException {
out.write(buff,off,len);
written += len;
}
public void flush() throws IOException {
out.flush();
}
public void close() throws IOException {
out.close();
}
}
/**
* The Default constructor creates the File and sets the
* UniformLogFormatter.
*/
protected FileandSyslogHandler( ) {
mAMXLoggingHook = AMXLoggingHook.getInstance();
try {
setFormatter( new UniformLogFormatter( ) );
} catch( Exception e ) {
new ErrorManager().error(
"FATAL ERROR: COULD NOT INSTANTIATE FILE AND SYSLOG HANDLER",
e, ErrorManager.GENERIC_FAILURE );
}
}
/**
* Creates the File under the specified instance directory
*/
public String createFileName( ) {
ServerContext sc = ApplicationServer.getServerContext();
String instDir = "";
if (sc != null) {
instDir = sc.getInstanceEnvironment().getInstancesRoot();
} else {
instDir = System.getProperty( INSTANCE_ROOT_PROPERTY );
}
String[] names = {instDir, LOGS_DIR, getLogFileName() };
// Create an absolute log filename
return StringUtils.makeFilePath(names, false);
}
/**
* Creates the file and initialized MeteredStream and passes it on to
* Superclass (java.util.logging.StreamHandler).
*/
private void openFile( String fileName ) throws IOException {
File file = new File( fileName );
FileOutputStream fout = new FileOutputStream( fileName, true );
BufferedOutputStream bout = new BufferedOutputStream( fout );
meter = new MeteredStream( bout, file.length() );
setOutputStream( meter );
}
/**
* Request Rotation called from Rotation Timer Task or LogMBean
*/
void requestRotation( ) {
synchronized( this ) {
rotationRequested = true;
}
}
/**
* cleanup the history log file based on JVM system property "max_history_files".
*
* If it is defined with valid number, we only keep that number of history logfiles;
* If "max_history_files" is defined without value, then default that number to be 10;
* If "max_history_files" is defined with value 0, no histry log file will
* be keeped; and server.log is the only log file.
*/
public void cleanUpHistoryLogFiles() {
String nStr = System.getProperty(LOGGING_MAX_HISTORY_FILES);
if (nStr==null) return;
int maxHistryFiles = 10;
if (!"".equals(nStr)) {
try {
maxHistryFiles = Integer.parseInt(nStr);
} catch (NumberFormatException e) {};
}
if (maxHistryFiles<0) return;
File dir = new File(absoluteFileName).getParentFile();
if (dir==null) return;
File[] fset = dir.listFiles();
ArrayList candidates = new ArrayList();
for (int i=0; fset!=null && i<fset.length; i++) {
if ( !logFileName.equals(fset[i].getName()) &&
fset[i].isFile() &&
fset[i].getName().startsWith(logFileName) ) {
candidates.add(fset[i].getAbsolutePath() );
}
}
if (candidates.size() <= maxHistryFiles) return;
Object[] pathes = candidates.toArray();
java.util.Arrays.sort(pathes);
try {
for (int i=0; i<pathes.length-maxHistryFiles; i++) {
new File((String)pathes[i]).delete();
}
} catch (Exception e) {
new ErrorManager().error("FATAL ERROR: COULD NOT DELETE LOG FILE..",
e, ErrorManager.GENERIC_FAILURE );
}
}
/**
* A Simple rotate method to close the old file and start the new one
* when the limit is reached.
*/
private void rotate( ) {
final FileandSyslogHandler thisInstance = this;
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction() {
public Object run( ) {
thisInstance.flush( );
thisInstance.close();
StringBuffer renamedFileName = null;
try {
File oldFile = new File( absoluteFileName );
renamedFileName =
new StringBuffer( absoluteFileName + "_" );
logRotateDateFormatter.format(
new Date(), renamedFileName,
new FieldPosition( 0 ) );
File rotatedFile = new File(
renamedFileName.toString() );
boolean renameSuccess = oldFile.renameTo( rotatedFile );
if( !renameSuccess ) {
// If we don't succeed with file rename which
// most likely can happen on Windows because
// of multiple file handles opened. We go through
// Plan B to copy bytes explicitly to a renamed
// file.
planBLogRotate(absoluteFileName,
renamedFileName.toString( ) );
String freshServerLogFileName = createFileName( );
// We do this to make sure that server.log
// contents are flushed out to start from a
// clean file again after the rename..
FileOutputStream fo =
new FileOutputStream( freshServerLogFileName );
fo.close( );
}
openFile( createFileName( ) );
LogFilter.setLogFile( new LogFile( absoluteFileName) );
// This will ensure that the log rotation timer
// will be restarted if there is a value set
// for time based log rotation
LogRotationTimer.getInstance( ).restartTimer( );
cleanUpHistoryLogFiles();
} catch( IOException ix ) {
new ErrorManager().error(
"FATAL ERROR: COULD NOT OPEN LOG FILE..", ix,
ErrorManager.OPEN_FAILURE );
}
return null;
}
}
);
}
/**
* This method is solely provided as a plan B for Windows. On Windows
* the file rename fails if there are open handles to the file.
* We want to make sure that the log rotation succeeds in this case also.
* Basically we copy the contents of the file instead of renaming.
*/
private void planBLogRotate( String originalFileName,
String renamedFileName )
{
FileOutputStream fo = null;
FileInputStream fi = null;
try {
fo = new FileOutputStream( renamedFileName );
fi = new FileInputStream( originalFileName );
int BUF_SIZE = 4096;
byte[] buffer = new byte[BUF_SIZE];
int i = -1;
do {
i = fi.read( buffer );
// Reached EOF.
if( i == -1 ) { break; }
if( i == BUF_SIZE ) {
fo.write( buffer );
} else {
// We have less number of bytes to read than the BUF_SIZE
// So, just create a temporary buffer with the exact
// number of bytes read and write that to the rotated
// file.
byte[] tempBuffer = new byte[i];
int j = 0;
for( j = 0; j < i; j++ ) {
tempBuffer[j] = buffer[j];
}
fo.write( tempBuffer );
}
} while( true );
} catch( Exception e ) {
// If we fail in Plan B. Too bad, we should not
// log a message here as it may lead to recursion.
} finally {
try {
fo.close( );
fi.close( );
} catch( Exception e ) { }
}
}
/**
* Publishes the logrecord using the super class and checks to see
* if a file rotation is required.
*/
public synchronized void publish( LogRecord record ) {
// This is provided to avoid the recursion. When Log Rotation
// is called, there is a chance that any of the called routines
// in the Log Rotation call path may log a message, which may
// result in a recursion. Hence, we shall log those log messages
// after the rotation is completed, so as to avoid the recursion.
if( rotationInProgress ) {
pendingLogRecordList.addLast(record);
return;
}
// Important: There are issues with double instantiation of
// FileandSyslogHandler if we move the createFile/openFile calls
// to the constructor. For now, we will do this for the first
// publish call.
if( meter == null ) {
try {
absoluteFileName = createFileName( );
openFile( absoluteFileName );
} catch( Exception e ) {
throw new RuntimeException(
"Serious Error Couldn't open Log File" + e );
}
}
super.publish( record );
flush( );
if ( ( rotationRequested )
|| ( ( limitForFileRotation > 0 )
&& ( meter.written >= limitForFileRotation ) ) )
{
rotationInProgress = true;
// If we have written more than the limit set for the
// file, or rotation requested from the Timer Task or LogMBean
// start fresh with a new file after renaming the old file.
rotate( );
rotationInProgress = false;
rotationRequested = false;
while (pendingLogRecordList.size() != 0) {
publish((LogRecord) pendingLogRecordList.removeFirst());
}
}
if (mAMXLoggingHook != null) {
mAMXLoggingHook.publish( record, getFormatter() );
}
int level = record.getLevel( ).intValue();
// The processing here is to raise alarms if
// alarm flag in domain.xml is on and the log level is SEVERE or
// WARNING
if(( level != WARNING ) && (level != SEVERE)) {
return;
}
//Any WARNING or SEVERE message is placed onto a singleton rolling
// error message list. This can be queried as part of a running
// server's status. This allows the last N error messages to be easily
// viewed without having to consult the log file.
String logMessage = getFormatter().format(record);
addRecentErrorMessage(logMessage);
ErrorStatistics.singleton().updateStatistics(record); // error stats.
if( logService == null ) {
logService = ServerLogManager.getLogService( );
}
// _REVISIT_: Deprecate Alarms flag
if( logService == null ) return;
// Raise an alarm if the logged message is a SEVERE or WARNING
// message.
if( record.getLevel( ).intValue() == WARNING ) {
LogMBean.getInstance().raiseWarningAlarm( record );
} else if( record.getLevel( ).intValue() == SEVERE ) {
LogMBean.getInstance().raiseSevereAlarm( record );
}
}
/**
*Returns the AMXLoggingHook to use for this logger.
*<p>
*This method is here primarily so a subclass can override it if needed.
*@return AMXLoggingHook to use
*/
protected AMXLoggingHook createAMXLoggingHook() {
return new AMXLoggingHook();
}
protected String getLogFileName() {
return logFileName;
}
}
|