/*
* This software and supporting documentation were developed by
*
* Siemens Corporate Technology
* Competence Center Knowledge Management and Business Transformation
* D-81730 Munich, Germany
*
* Authors (representing a really great team ;-) )
* Stefan B. Augustin, Thorbj�rn Hansen, Manfred Langen
*
* This software is Open Source under GNU General Public License (GPL).
* Read the text of this license in LICENSE.TXT
* or look at www.opensource.org/licenses/
*
* Once more we emphasize, that:
* THIS SOFTWARE IS MADE AVAILABLE, AS IS, WITHOUT ANY WARRANTY
* REGARDING THE SOFTWARE, ITS PERFORMANCE OR
* FITNESS FOR ANY PARTICULAR USE, FREEDOM FROM ANY COMPUTER DISEASES OR
* ITS CONFORMITY TO ANY SPECIFICATION. THE ENTIRE RISK AS TO QUALITY AND
* PERFORMANCE OF THE SOFTWARE IS WITH THE USER.
*
*/
// KFMLog
// ************ package ******************************************************
package KFM.log;
// ************ imports ******************************************************
import KFM.DateTimeServices.*;
import KFM.Converter;
import KFM.File.*;
import KFM.Exceptions.*;
import java.io.*;
import java.util.*;
import KFM.Smtp;
/** Provide logging for servlets and library classes.
*
* <p>Every servlet has a member variable mLog of class KFMLog for writing logging
* information. LogFileName, LogFileLevel and LogFileOriginator should be configured
* in the servlet config file, f.e.:
* LogFileName=o:/KFM/logs/SieCD2.log
* LogFileLevel=2
* LogFileOriginator=SieCD2@dampfer</p>
*
* <p>Currently four levels are supported: error (0), info (1), detail (3), debug (4)</p>
*
* <p>Logging information can be written by invoking mLog.xxx, where xxx can be one of the
* methods error, info, detail, debug, f.e.:<br>
* <code>mLog.error("Log application log and default log", new ProgrammerException("TEST"));</code></p>
*
* <p>If the invoking level is equal or lower the current logfile level then the information
* will be written to the log file.</p>
*
* <H2>Secondary logs</H2>
*
* <p>For test cases it is possible to write log information in two different log files.
* Let us say that you want to debug a DB error in Servlet Ex3. Let us say that Ex3 has 73456
* different log entries that keep confusing you. So it would be handy to have a log file
* that only contains the parts that you are interested in. You can use secondary logs to
* do that: log all relevant database information in a file DBEx3.log and also keep it in
* the servlet log file Ex3.log (mLog). Code it like this:</p>
*
* <pre>
* KFMLog dblog = KFMLog.getInstance(
* "DBEx3", //Originator
* "o:/KFM/logs/DBEx3.log", //Logfile
* KFMLog.DETAIL_LEVEL, //Detaillevel
* mLog, //Also log to
* "x,y@abc.de", //AdminEmail
* "mail.host.de"); //MailHost
* </pre>
*
*/
public class KFMLog {
// ************************************************************
// Inner classes
// ************************************************************
/**
* <P> An inner class to encapsulate a log message repeated several times within some time span.
* Objects of this class remember
* <OL>
* <LI> the log message (a String) </LI>
* <LI> the time stamp (a Date object) the log message was produced first </LI>
* <LI> the number of times (an integer) the log message was produced </LI>
* </OL>
* </P>
*
* @see mRepeatedLogMessageMemory
*/
private static class RepeatedLogMessage
{
public String logMessage;
public Date firstTime;
public int nrOfTimes;
}
// ************************************************************
// Constants
// ************************************************************
/**
* The maximum size of the log file, currently 5 MB
*/
protected static final long MAX_FILE_SIZE = 5*1024*1024;
/**
* The maximum number of backup log files, currently 10
*/
protected static final int MAX_BACKUP_LOG_FILES = 10;
/**
* Turns the log off, no messages are logged
*/
public static final int DEACTIVATE_LEVEL = 0;
/**
* This level should be used for fatal errors, normal errors and warnings.
*/
public static final int ERROR_LEVEL = 1;
/**
* This level should be used for SQL logging.
*/
public static final int INFO_LEVEL = 2;
/**
* This level should be used for more detailed messages.
*/
public static final int DETAIL_LEVEL = 3;
/**
* This level should be used only for debugging. Never in the production
* release.
*/
public static final int DEBUG_LEVEL = 4;
/**
* This String separates the items within one line of logging.
*/
protected static final String cSeparator = " | ";
/**
* The amount of milliseconds within which the same message should not be sent twice per mail.
*/
protected static final long cMillisToSuppressMail = 30000;
// ************************************************************
// Variables
// ************************************************************
protected PrintStream mPrintStream;
protected int mLoggingLevel;
protected long mFileSize;
protected String mLogFileName;
protected boolean mLoggingEnabled;
protected String mOriginator;
protected String mMailHost;
protected String mAdminMail;
/**
* A Hashtable to store the different KFMLogs in use, identified by their originators.
*/
static protected Hashtable mLogs = new Hashtable();
/**
* Second log file.
* This can be used if you want to have one log file for servlet and DB
* logs and a second one only for the DB logs
*/
protected KFMLog mSecondLog=null;
/**
* A Hashtable to memorize what log messages have been produced repeatedly.
* Needed, to ensure that the same log message is not sent twice
* within the time span of cMillisToSuppressMail.
*
* The keys are the log message (the subject of the mail),
* the values are objects of the inner class RepeatedLogMessage.
*/
protected Hashtable mRepeatedLogMessageMemory = new Hashtable();
/**
* The name of the server on which our software is running
*/
protected static final String mHostname=getHostName();
// ************************************************************
// static Method
// ************************************************************
/** Return the key for the hashtable belonging to aOriginator.
*/
private static String getOriginatorKey(String aOriginator)
{
return aOriginator + "(" + mHostname + ")";
}
/** Return the log file belonging to the servlet `aOriginator�.
*
* <P>Every Servlet has its own log file. But every servlet instance
* (of the same Servlet) should use the same log file. Therefore we
* use the singleton pattern with a hash that maps servlets to log files.
* Different servlets however (f.e. SieCD2 and SieMap) should use
* different log files. Therefore we use an Originator to identify
* servlet instances belonging to the same servlet.</P>
*/
public static synchronized KFMLog getInstance(
String aOriginator,
String aLogFileName,
int aLogLevel,
String aAdminMail,
String aMailHost)
{
KFMLog tLog = (KFMLog) mLogs.get(getOriginatorKey(aOriginator));
if(null == tLog) {
tLog = new KFMLog(aOriginator, aLogFileName, aLogLevel);
mLogs.put(getOriginatorKey(aOriginator), tLog);
}
tLog.setAdminMail(aAdminMail);
tLog.setMailHost(aMailHost);
return tLog;
}
/** Return the log file belonging to the servlet `aOriginator� and set a secondary log.
*
* The secondary Log will be set to aSecondLog. For an explanation of secondary logs,
* read the class docu above.
*/
public static synchronized KFMLog getInstance(
String aOriginator,
String aLogFileName,
int aLogLevel,
KFMLog aSecondLog,
String aAdminMail,
String aMailHost)
{
KFMLog tLog = getInstance(aOriginator, aLogFileName, aLogLevel, aAdminMail, aMailHost);
tLog.setSecondaryLog(aSecondLog);
return tLog;
}
/** Return the log file belonging to the servlet `aOriginator�.
*
* <P>This method may be called from within a class that needs access to
* (the one) log file of the originator servlet, but that does not know
* about the 'other' log attributes (like the log file name).</P>
*
* <P>If you are sure, that some other object has already created the log file before
* (by use of the other getInstance() methods), use this method.
* If this assumption does not hold, however, this method will return null,
* so you better check for this.</P>
*
* @param aOriginator a String identifying the log instance
* @return the KFMLog object for this originator (or null)
*/
public static synchronized KFMLog getInstance(String aOriginator)
{
return (KFMLog) mLogs.get(getOriginatorKey(aOriginator));
}
/**
* This method returns the hashtable containing all registered KFMLogs. It will
* be invoked by the admin servlet to display the current log levels to the administrator.
* The administrator has the possiblity to modify the log level at runtime.
*/
public static synchronized Hashtable getRegisteredLogs()
{
return mLogs;
}
/**
* This method returns the name of the host on which the software is running.
*/
protected static String getHostName() {
String tHostname="Unkown";
try {
tHostname= java.net.InetAddress.getLocalHost().getHostName();
} catch (Exception e){}
return tHostname;
}
// ************************************************************
// Methods
// ************************************************************
/** Constructor for KFMLog.
*
* It opens a PrintWriter for the given logFile aLogFile. The
* logging level will be set to aLogLevel.
*
* @param aOriginator Originator of the log file
* @param aLogFileName LogFile with complete path
* @param aLogLevel Logging Level
*/
private KFMLog (
String aOriginator,
String aLogFileName,
int aLogLevel)
{
mLoggingEnabled = false;
if (aOriginator == null){
throw new ProgrammerException("Wrong Originator!");
} else if (aLogFileName == null){
throw new ProgrammerException("Wrong LogFileName!");
} else if ((aLogLevel > DEBUG_LEVEL) || (aLogLevel < DEACTIVATE_LEVEL)){
throw new ProgrammerException("Wrong LogLevel!");
} else {
try {
// open the file aLogFile in append and autoflush mode
mFileSize = (new File(aLogFileName)).length();
FileOutputStream tFileOutputStream = new FileOutputStream(aLogFileName, true);
mPrintStream = new PrintStream(new BufferedOutputStream(tFileOutputStream, 4096) , true);
mLoggingLevel = aLogLevel;
mLogFileName = aLogFileName;
mLoggingEnabled = true;
mOriginator = aOriginator+"("+mHostname+")";
} catch (IOException e) {
mLoggingEnabled = false;
throw new ProgrammerException("Couldn't write to log file!\n" +
"Path:" + aLogFileName + "\n" +
Converter.exceptionStackToString(e));
}
}
}
/** */
protected KFMLog ()
{
}
/**
* Logs a text string at the logging level specified.
*
* This method is used when your code needs to parameterize
* the logging level. Otherwise you probably should use the
* error(), warning(), info() methods below, they will be less verbose in your
* code.
*
* Note: We moved the synchronize block into the method to avoid performance problems.
*/
public void logString(
String aOriginator,
String aLogString,
int aErrorLevel)
{
String tErrorLevel = getErrorLevelString(aErrorLevel);
// Events with same or lower Level as mLoggingLevel will be logged
if (mLoggingEnabled && (aErrorLevel <= mLoggingLevel)){
String tMessage =
KFM_DateTimeService.createTimeStamp() + cSeparator +
tErrorLevel + cSeparator +
aOriginator + cSeparator +
Thread.currentThread().getName()+ cSeparator +
aLogString;
synchronized(this) {
// if current logFile reaches max size, create new one and save old
// files
if (mFileSize >= MAX_FILE_SIZE){
createNewLogFileAndSaveOldFiles();
}
// write logString in logFile if errorLevel is equal or less mLoggingLevel
mFileSize += tMessage.length();
mPrintStream.println(tMessage);
// We use a BufferedOutputStream, therefore flushing is done automatically
//mPrintStream.flush();
}
}
if (mSecondLog != null){
mSecondLog.logString(aOriginator, aLogString, aErrorLevel);
}
}
/**
* Returns the String belonging to the given errorLevel
*/
public static String getErrorLevelString (int aErrorLevel)
{
String tErrorLevel="DEACTIVATE_LEVEL";
switch (aErrorLevel){
case ERROR_LEVEL: tErrorLevel="ERROR_LEVEL";
break;
case INFO_LEVEL: tErrorLevel="INFO_LEVEL";
break;
case DETAIL_LEVEL: tErrorLevel="DETAIL_LEVEL";
break;
case DEBUG_LEVEL: tErrorLevel="DEBUG_LEVEL";
}
return tErrorLevel;
}
/**
* Returns the errorLevel as int
*/
public static int getErrorLevelInt (String aErrorLevel)
{
if (aErrorLevel.equals("ERROR_LEVEL")){
return ERROR_LEVEL;
} else if (aErrorLevel.equals("INFO_LEVEL")){
return INFO_LEVEL;
} else if (aErrorLevel.equals("DETAIL_LEVEL")){
return DETAIL_LEVEL;
} else if (aErrorLevel.equals("DEBUG_LEVEL")){
return DEBUG_LEVEL;
}
return DEACTIVATE_LEVEL;
}
/**
* Logs a text string at ERROR_LEVEL and sends an email.
* The email subject and email content both contain aLogString.
*
* @deprecated use #error(String, String) instead
*/
public void error(String aLogString)
{
logString(mOriginator, aLogString, ERROR_LEVEL);
sendMail("ERROR "+ mOriginator + ": " +aLogString, "");
}
/**
* Logs the concatenation of aLogString and an exception stack trace
* at ERROR_LEVEL and sends an email.
* The email subject consists of aLogString and the
* email content consists of the concatenation of aLogString
* and the exception stack trace.
*
* @deprecated use #error(String, String, Throwable) instead
*/
public void error(String aLogString, Throwable aException)
{
if (aException == null) {
error(aLogString);
return;
}
// Get a stack trace into a string.
String tStackTrace = Converter.exceptionStackToString(aException);
logString(mOriginator, aLogString+"\n"+tStackTrace, ERROR_LEVEL);
sendMail("ERROR "+ mOriginator + ": " +aLogString, tStackTrace);
}
/**
* Logs the concatenation of the subject and content string at ERROR_LEVEL
* and sends an email.
* The email subject consists of aLogStringSubject and the
* email content consists of aLogStringMessage.
*
* 2003-05-02: The email subjects are often very long and thus very hard to read in Outlook.
* So I find it helpful to repeat `aLogStringSubject� in the mail body. If you disagree, then let's
* vote in the team meeting.
*/
public void error(String aLogStringSubject, String aLogStringMessage)
{
logString(mOriginator, aLogStringSubject + ":" + aLogStringMessage, ERROR_LEVEL);
sendMail("ERROR "+ mOriginator + ": " +aLogStringSubject,
aLogStringSubject + "\n" + aLogStringMessage, "");
}
/**
* Logs the concatenation of the subject string, the content string
* and an exception stack trace at ERROR_LEVEL and sends an email.
* The email subject consists of aLogStringSubject and the
* email content consists of the concatenation of aLogStringMessage
* and the exception stack trace.
*
* 2003-05-02: The email subjects are often very long and thus very hard to read in Outlook.
* So I find it helpful to repeat `aLogStringSubject� in the mail body. If you disagree, then let's
* vote in the team meeting.
*/
public void error(String aLogStringSubject, String aLogStringMessage, Throwable aException)
{
if (aException == null) {
error(aLogStringSubject, aLogStringMessage);
return;
}
// Get a stack trace into a string.
String tStackTrace = Converter.exceptionStackToString(aException);
logString(mOriginator, aLogStringSubject + ":" + aLogStringMessage+"\n"+tStackTrace, ERROR_LEVEL);
sendMail("ERROR "+ mOriginator + ": " +aLogStringSubject,
aLogStringSubject + "\n" + aLogStringMessage, tStackTrace);
}
/**
* Logs a text string at INFO_LEVEL
*/
public void info(String aLogString)
{
logString(mOriginator, aLogString, INFO_LEVEL);
}
/**
* Logs a text string and an exception at INFO_LEVEL
*/
public void info(String aLogString, Exception aException)
{
if (aException == null) {
info(aLogString);
return;
}
// Get a stack trace into a string.
String tStackTrace = Converter.exceptionStackToString(aException);
logString(mOriginator, aLogString+"\n"+tStackTrace, INFO_LEVEL);
}
/**
* Logs a text string at DETAIL_LEVEL
*/
public void detail(String aLogString)
{
logString(mOriginator, aLogString, DETAIL_LEVEL);
}
/**
* Logs a text string at DEBUG_LEVEL
*/
public void debug(String aLogString)
{
logString(mOriginator, aLogString, DEBUG_LEVEL);
}
/**
* Tests whether the log is deactivated, i.e. whether we debug everything
* at level DEACTIVATE_LEVEL or below.
*/
public boolean isDeactivateLevel()
{
return (mLoggingLevel >= DEACTIVATE_LEVEL);
}
/**
* Tests whether the log is in Error level, i.e. whether we debug everything
* at level ERROR_LEVEL or below.
*/
public boolean isErrorLevel()
{
return (mLoggingLevel >= ERROR_LEVEL);
}
/**
* Tests whether the log is in Info level, i.e. whether we debug everything
* at level INFO_LEVEL or below.
*/
public boolean isInfoLevel()
{
return (mLoggingLevel >= INFO_LEVEL);
}
/**
* Tests whether the log is in Detail level, i.e. whether we debug everything
* at level DETAIL_LEVEL or below.
*/
public boolean isDetailLevel()
{
return (mLoggingLevel >= DETAIL_LEVEL);
}
/**
* Tests whether the log is in Debug level, i.e. whether we debug everything
* at level DEBUG_LEVEL or below.
*/
public boolean isDebugLevel()
{
return (mLoggingLevel >= DEBUG_LEVEL);
}
/**
* Gets the current logging level
*/
public int getLogLevel()
{
return mLoggingLevel;
}
/**
* Sets the current logging level
*/
public void setLogLevel(int aLogLevel)
{
mLoggingLevel = aLogLevel;
}
/**
* Gets the originator
*/
public String getOriginator()
{
return mOriginator;
}
/**
* Gets the log file name
*/
public String getLogFileName()
{
return mLogFileName;
}
/**
* Sets the secondary Log
*/
public void setSecondaryLog(KFMLog aSecondLog)
{
mSecondLog=aSecondLog;
}
/**
* Sets the mail host
*/
public void setMailHost(String aMailHost)
{
mMailHost=aMailHost;
}
/**
* Sets the admin email address
*/
public void setAdminMail(String aAdminMail)
{
mAdminMail=aAdminMail;
}
/**
* Gets the mail host
*/
public String getMailHost()
{
return mMailHost;
}
/**
* Gets the admin email address (may also be null)
*/
public String getAdminMail()
{
return mAdminMail;
}
/**
* Sends mail to receivers (only if mAdminMail and mMailHost are set).
* Don't send mail twice within cMillisToSuppressMail.
* It uses a given subject and a given message inside the mail.
*/
protected void sendMail (
String aSubject,
String aMessage,
String aStackTrace)
{
// nothing to do if mAdminMail or mMailHost are not set
if ((mAdminMail == null) || (mMailHost == null)) {
return;
}
// Hava a look at mRepeatedLogMessageMemory
// Did we sent class within the last cMillisToSuppressMail ?
RepeatedLogMessage tRepLog = (RepeatedLogMessage) mRepeatedLogMessageMemory.get(aMessage);
boolean tSend = true;
String tRepetition = "";
if (tRepLog == null) {
// Log message never happened before: we send the mail.
// Additionally produce a RepeatedLogMessage item and store it within mLogMessageMemory.
tRepLog = new RepeatedLogMessage();
tRepLog.logMessage = aMessage;
tRepLog.firstTime = new Date();
tRepLog.nrOfTimes = 1;
mRepeatedLogMessageMemory.put(aMessage, tRepLog);
} else {
// Log message happened before: test if this entry is older than cMillisToSuppressMail.
long tMillisDifference = (new Date()).getTime() - tRepLog.firstTime.getTime();
if (tMillisDifference > cMillisToSuppressMail) {
// It is older: we send the mail.
// Additionally, we prepare a repetition info string and
// reset the values of the RepeatedLogMessage item.
tRepLog.firstTime = new Date();
if (tRepLog.nrOfTimes > 1) {
tRepetition = "... This message happened " + (tRepLog.nrOfTimes - 1) +
" times within the last " + (cMillisToSuppressMail/1000) + " seconds ...\n\n";
}
tRepLog.nrOfTimes = 1;
} else {
// it is not older: we don't send the mail.
// Instead, we increase nrOfTimes
tSend = false;
tRepLog.nrOfTimes += 1;
}
}
if (tSend) {
// Construct the SMTP message
String tSmtpMessage = "Subject: " + aSubject + "\n\n"
+ tRepetition + KFM_DateTimeService.createTimeStamp() + ": " + aMessage+ "\n"
+ Thread.currentThread().getName()+"\n"+aStackTrace;
// the "from" header may only contain one e-mail address, otherwise mail is ignored,
// therefore when mAdminMail contains several addresses, we use only the first as sender
String tFrom = mAdminMail;
int tIndex = tFrom.indexOf(',');
if (tIndex != -1) {
tFrom = tFrom.substring(0, tIndex);
}
// *Send* mail via Smtp class. Yes, really, the constructor *sends* a mail.
Smtp tMail = new Smtp(mMailHost, mAdminMail, tFrom, tSmtpMessage, true);
}
}
/**
* Sends mail to receivers (only if mAdminMail and mMailHost are set).
* Use the message as subject of the mail.
* Don't send mail twice within cMillisToSuppressMail.
*
* @deprecated
*/
protected void sendMail (
String aMessage,
String aStackTrace)
{
sendMail(aMessage, aMessage, aStackTrace);
}
/**
* Creates a new log file with the name mLogFileName and stores the old log files.
* The old log files will be renamed to mLogFileName1, mLogFileName2, ..., mLogFileName10.
* The oldest log file will be overwritten.
*/
private void createNewLogFileAndSaveOldFiles()
{
// close current log file
mPrintStream.flush();
mPrintStream.close();
try {
// rename existing log files
for(int i = MAX_BACKUP_LOG_FILES - 1; i >= 0; i--) {
// special case handling: there is no 'log0' it's just 'log'
FileUtils.moveFile(
new File(mLogFileName + (0 == i ? "" : "" + i)),
new File(mLogFileName + (i+1)));
}
// open new log file (with the same name)
FileOutputStream tFileOutputStream =
new FileOutputStream(mLogFileName);
mPrintStream = new PrintStream(
new BufferedOutputStream(tFileOutputStream, 4096), true /* autoflush */);
mFileSize = 0;
mLoggingEnabled = true;
} catch (IOException ioe){
sendMail("CRITICAL ERROR: Logging will be disabled",
Converter.exceptionStackToString(ioe));
mLoggingEnabled = false;
}
}
}