/* * Copyright 2006 Holger West, Ralf Joachim * * Licensed 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 log4j; import org.apache.log4j.AppenderSkeleton; import org.apache.log4j.spi.ErrorCode; import org.apache.log4j.spi.LoggingEvent; import org.exolab.castor.jdo.Database; import org.exolab.castor.jdo.JDOManager; import org.exolab.castor.jdo.Query; import org.exolab.castor.jdo.QueryResults; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; /** * The <code>CastorAppender</code> provides sending log events to a database. * * <p>Each append call adds the <code>LoggingEvent</code> to an <code>ArrayList</code> * buffer. When the buffer is filled each log event is saved to the database. * * <b>DatabaseName</b>, <b>BufferSize</b>, <b>ColumnWidthClass</b>, * <b>ColumnWidthThread</b>, <b>ColumnWidthMessage</b>, <b>ColumnWidthStackTrace</b> and * <b>DuplicateCount</b> are configurable options in the standard log4j ways. * * @author <a href="mailto:holger.west@syscon-informatics.de">Holger West</a> */ public final class CastorAppender extends AppenderSkeleton { // ----------------------------------------------------------------------------------- /** Default column width for the class column. */ private static final int COLUMNWIDTHCLASS = 100; /** Default column width for the thread column. */ private static final int COLUMNWIDTHTHREAD = 100; /** Default column width for the message column. */ private static final int COLUMNWIDTHMESSAGE = 1000; /** Default column width for the message column. If this value is greater than 4000 * and using an oracle database, as minimum the 10g driver is necessary. */ private static final int COLUMNWIDTHSTACKTRACE = 20000; /** Should duplicate entries be replaced with the newest one and count the number of * occurrence or should all records be saved independent. If set to false, all * records are saved independent. */ private static final boolean DUPLICATECOUNT = false; /** List holding all registered <code>CastorAppenders</code>. */ private static List _elements = new ArrayList(); /** Default size of LoggingEvent buffer before writting to the database. */ private int _bufferSize = 1; /** ArrayList holding the buffer of Logging Events. */ private ArrayList _buffer; /** Helper object for clearing out the buffer. */ private ArrayList _removes; /** The database is opened the first time it is needed and then held open until the * appender is closed. */ private Database _database; /** A prepared statement to identify a possible existing entry with the same values. * It is only used if 'duplicateCount' is enabled. */ private Query _qry; /** The name of the database to be used by castor. This <b>must</b> be specified in * the log4j configuration. */ private String _databaseName; /** Column width for the class information. */ private int _columnWidthClass = COLUMNWIDTHCLASS; /** Column width for the thread information. */ private int _columnWidthThread = COLUMNWIDTHTHREAD; /** Column width for the message information. */ private int _columnWidthMessage = COLUMNWIDTHMESSAGE; /** Column width for the stack trace information. */ private int _columnWidthStackTrace = COLUMNWIDTHSTACKTRACE; /** Replace duplicate entries and count the occurrence? This can be very slow when * saving to the database. */ private boolean _duplicateCount = DUPLICATECOUNT; // ----------------------------------------------------------------------------------- /** * Add a new <code>CastorAppender</code> to static list. * * @param appender The <code>CastorAppender</code> to be added. */ private static synchronized void addAppender(final CastorAppender appender) { _elements.add(appender); } /** * Remove a <code>CastorAppender</code> from static list. * * @param appender The <code>CastorAppender</code> to be removed. */ private static synchronized void removeAppender(final CastorAppender appender) { _elements.remove(appender); } /** * Get an array holding all registered <code>CastorAppender</code>. * * @return An array holding all registered <code>CastorAppender</code>. */ private static synchronized CastorAppender[] getAppenders() { CastorAppender[] appenders = new CastorAppender[_elements.size()]; return (CastorAppender[]) _elements.toArray(appenders); } /** * When the program has ended all logger instances are destroyed. To save all data * which are still in the buffer, this method must be called. It saves all data from * all registered <code>CastorAppender</code>. * <br/> * As an alternative <code>org.apache.log4j.LogManager.shutdown()</code> can be * called. */ public static void flush() { CastorAppender[] appenders = getAppenders(); if (appenders.length > 0) { for (int i = 0; i < appenders.length; i++) { appenders[i].flushBuffer(); } } } // ----------------------------------------------------------------------------------- /** * Default constructor. */ public CastorAppender() { super(); addAppender(this); _database = null; _buffer = new ArrayList(_bufferSize); _removes = new ArrayList(_bufferSize); } /** Closes the appender before disposal. */ public void finalize() { close(); } /** * Closes the appender, flushing the buffer first then closing the query and database * if it is still open. */ public void close() { flushBuffer(); if (_database != null) { try { _qry.close(); _database.close(); } catch (Exception e) { errorHandler.error("Error closing database.", e, ErrorCode.CLOSE_FAILURE); } } this.closed = true; removeAppender(this); } // ----------------------------------------------------------------------------------- /** * Adds the event to the buffer. When full the buffer is flushed. * * @param event The event to be logged. */ public synchronized void append(final LoggingEvent event) { _buffer.add(event); if (_buffer.size() >= _bufferSize) { flushBuffer(); } } /** * Loops through the buffer of <code>LoggingEvents</code> and store them into the * database. If a statement fails the <code>LoggingEvent</code> stays in the buffer! */ private synchronized void flushBuffer() { _removes.ensureCapacity(_buffer.size()); Database db = getDatabase(); try { for (Iterator i = _buffer.iterator(); i.hasNext();) { LoggingEvent logEvent = (LoggingEvent) i.next(); execute(logEvent); _removes.add(logEvent); } db.commit(); _buffer.removeAll(_removes); _removes.clear(); } catch (Exception e) { errorHandler.error("Error flush buffer.", e, ErrorCode.GENERIC_FAILURE); } } /** * Initialize the database and create the query. If the database is already * initialized, only return the database. In both cases a transaction is started. * * @return The initialized database. */ private Database getDatabase() { if (_database == null) { try { _database = JDOManager.createInstance(_databaseName).getDatabase(); _database.begin(); String oql = "select o from " + LogEntry.class.getName() + " o where o.className = $1 and" + " o.level = $2 and" + " o.message = $3"; _qry = _database.getOQLQuery(oql); } catch (Exception e) { errorHandler.error("Error get database.", e, ErrorCode.GENERIC_FAILURE); } } else { try { _database.begin(); } catch (Exception e) { errorHandler.error( "Cannot begin a transaction.", e, ErrorCode.GENERIC_FAILURE); } } return _database; } /** * Save the given <code>LoggingEvent</code> to the database. If 'duplicateCount' is * enabled, a possible earlier entry is updated. Events with exceptions are stored * ever. * * @param event The <code>LoggingEvent</code> to be saved. */ private void execute(final LoggingEvent event) { LogEntry entry; if (event.getMessage() instanceof LogEntry) { entry = (LogEntry) event.getMessage(); } else if (event.getMessage() != null) { String message = event.getMessage().toString(); message = clipLength(message, _columnWidthMessage); entry = new LogEntry(message); } else { entry = new LogEntry(); } String clazz = event.getLoggerName(); clazz = clipLength(clazz, _columnWidthClass); entry.setClassName(clazz); String thread = event.getThreadName(); thread = clipLength(thread, _columnWidthThread); entry.setThread(thread); entry.setLevel(event.getLevel().toString()); entry.setTimestamp(new Date(event.timeStamp)); //----------------------------------------------------------------- boolean hasException = (event.getThrowableInformation() != null); if (hasException) { if (_columnWidthStackTrace > 0) { LogExceptionEntry exceptionEntry = new LogExceptionEntry(); String temp = ""; String[] stackTrace = event.getThrowableStrRep(); int stackSize = stackTrace.length; for (int i = 0; i < stackSize; i++) { temp = temp.concat(stackTrace[i] + "\n"); } temp = clipLength(temp, _columnWidthStackTrace); exceptionEntry.setStackTrace(temp); exceptionEntry.setEntry(entry); entry.setException(exceptionEntry); } } //----------------------------------------------------------------- try { if (!hasException && _duplicateCount) { _qry.bind(entry.getClassName()); _qry.bind(entry.getLevel()); _qry.bind(entry.getMessage()); QueryResults rst = _qry.execute(); if (rst.hasMore()) { LogEntry x = (LogEntry) rst.next(); x.setTimestamp(entry.getTimestamp()); x.setThread(entry.getThread()); x.setCount(new Integer(x.getCount().intValue() + 1)); } else { entry.setCount(new Integer(1)); _database.create(entry); } rst.close(); } else { entry.setCount(new Integer(1)); _database.create(entry); } } catch (Exception e) { errorHandler.error("Cannot save the object.", e, ErrorCode.FLUSH_FAILURE); } } /** * Clip a string to ensure the length. If the string is longer, the rear part is * clipped. * * @param value The string to cut. * @param maxLength The maximum length of this value. * @return The clipped String. */ private String clipLength(final String value, final int maxLength) { if (value.length() > maxLength) { return value.substring(0, maxLength); } return value; } /** * CastorAppender don't requires a layout. * * @return <code>true</code> if this appender require a layout, otherwise * <code>false</code>. * */ public boolean requiresLayout() { return false; } // ----------------------------------------------------------------------------------- /** * Set the size of the buffer. * * @param newBufferSize New size of the buffer. */ public void setBufferSize(final int newBufferSize) { _bufferSize = newBufferSize; _buffer.ensureCapacity(_bufferSize); _removes.ensureCapacity(_bufferSize); } /** * Get the size of the buffer. * * @return The size of the buffer. */ public int getBufferSize() { return _bufferSize; } /** * Set the name of the database. * * @param name Name of the database. */ public void setDatabaseName(final String name) { _databaseName = name; } /** * Get the name of the database. * * @return Name of the database. */ public String getDatabaseName() { return _databaseName; } /** * Set the column width for class information. * * @param columWidth The column width for class information. */ public void setColumnWidthClass(final int columWidth) { _columnWidthClass = columWidth; } /** * Get the column width for class information. * * @return The column width for class information. */ public int getColumnWidthClass() { return _columnWidthClass; } /** * Set the column width for thread information. * * @param columWidth The column width for thread information. */ public void setColumnWidthThread(final int columWidth) { _columnWidthThread = columWidth; } /** * Get the column width for tread information. * * @return The column width for thread information. */ public int getColumnWidthThread() { return _columnWidthThread; } /** * Set the column width for message information. * * @param columWidth The column width for message information. */ public void setColumnWidthMessage(final int columWidth) { _columnWidthMessage = columWidth; } /** * Get the column width for message information. * * @return The column width for message information. */ public int getColumnWidthMessage() { return _columnWidthMessage; } /** * Set the column width for stack trace information. * * @param columWidth The column width for stack trace information. */ public void setColumnWidthStackTrace(final int columWidth) { _columnWidthStackTrace = columWidth; } /** * Get the column width for stack trace information. * * @return The column width for stack trace information. */ public int getColumnWidthStackTrace() { return _columnWidthStackTrace; } /** * Set duplicate count. * * @param duplicateCount Should duplicate count be enabled? */ public void setDuplicateCount(final String duplicateCount) { String temp = duplicateCount.toLowerCase(); if ("true".equals(temp)) { _duplicateCount = true; } else { _duplicateCount = false; } } /** * Is duplicate count enabled? * * @return <code>true</code> if duplicate count is enabled, otherwise * <code>false</code>. */ public String getDuplicateCount() { return new Boolean(_duplicateCount).toString(); } // ----------------------------------------------------------------------------------- }