001: /*
002: * Copyright 2006 Holger West, Ralf Joachim
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package log4j;
017:
018: import org.apache.log4j.AppenderSkeleton;
019: import org.apache.log4j.spi.ErrorCode;
020: import org.apache.log4j.spi.LoggingEvent;
021: import org.exolab.castor.jdo.Database;
022: import org.exolab.castor.jdo.JDOManager;
023: import org.exolab.castor.jdo.Query;
024: import org.exolab.castor.jdo.QueryResults;
025:
026: import java.util.ArrayList;
027: import java.util.Date;
028: import java.util.Iterator;
029: import java.util.List;
030:
031: /**
032: * The <code>CastorAppender</code> provides sending log events to a database.
033: *
034: * <p>Each append call adds the <code>LoggingEvent</code> to an <code>ArrayList</code>
035: * buffer. When the buffer is filled each log event is saved to the database.
036: *
037: * <b>DatabaseName</b>, <b>BufferSize</b>, <b>ColumnWidthClass</b>,
038: * <b>ColumnWidthThread</b>, <b>ColumnWidthMessage</b>, <b>ColumnWidthStackTrace</b> and
039: * <b>DuplicateCount</b> are configurable options in the standard log4j ways.
040: *
041: * @author <a href="mailto:holger.west@syscon-informatics.de">Holger West</a>
042: */
043: public final class CastorAppender extends AppenderSkeleton {
044: // -----------------------------------------------------------------------------------
045:
046: /** Default column width for the class column. */
047: private static final int COLUMNWIDTHCLASS = 100;
048:
049: /** Default column width for the thread column. */
050: private static final int COLUMNWIDTHTHREAD = 100;
051:
052: /** Default column width for the message column. */
053: private static final int COLUMNWIDTHMESSAGE = 1000;
054:
055: /** Default column width for the message column. If this value is greater than 4000
056: * and using an oracle database, as minimum the 10g driver is necessary. */
057: private static final int COLUMNWIDTHSTACKTRACE = 20000;
058:
059: /** Should duplicate entries be replaced with the newest one and count the number of
060: * occurrence or should all records be saved independent. If set to false, all
061: * records are saved independent. */
062: private static final boolean DUPLICATECOUNT = false;
063:
064: /** List holding all registered <code>CastorAppenders</code>. */
065: private static List _elements = new ArrayList();
066:
067: /** Default size of LoggingEvent buffer before writting to the database. */
068: private int _bufferSize = 1;
069:
070: /** ArrayList holding the buffer of Logging Events. */
071: private ArrayList _buffer;
072:
073: /** Helper object for clearing out the buffer. */
074: private ArrayList _removes;
075:
076: /** The database is opened the first time it is needed and then held open until the
077: * appender is closed. */
078: private Database _database;
079:
080: /** A prepared statement to identify a possible existing entry with the same values.
081: * It is only used if 'duplicateCount' is enabled. */
082: private Query _qry;
083:
084: /** The name of the database to be used by castor. This <b>must</b> be specified in
085: * the log4j configuration. */
086: private String _databaseName;
087:
088: /** Column width for the class information. */
089: private int _columnWidthClass = COLUMNWIDTHCLASS;
090:
091: /** Column width for the thread information. */
092: private int _columnWidthThread = COLUMNWIDTHTHREAD;
093:
094: /** Column width for the message information. */
095: private int _columnWidthMessage = COLUMNWIDTHMESSAGE;
096:
097: /** Column width for the stack trace information. */
098: private int _columnWidthStackTrace = COLUMNWIDTHSTACKTRACE;
099:
100: /** Replace duplicate entries and count the occurrence? This can be very slow when
101: * saving to the database. */
102: private boolean _duplicateCount = DUPLICATECOUNT;
103:
104: // -----------------------------------------------------------------------------------
105:
106: /**
107: * Add a new <code>CastorAppender</code> to static list.
108: *
109: * @param appender The <code>CastorAppender</code> to be added.
110: */
111: private static synchronized void addAppender(
112: final CastorAppender appender) {
113: _elements.add(appender);
114: }
115:
116: /**
117: * Remove a <code>CastorAppender</code> from static list.
118: *
119: * @param appender The <code>CastorAppender</code> to be removed.
120: */
121: private static synchronized void removeAppender(
122: final CastorAppender appender) {
123: _elements.remove(appender);
124: }
125:
126: /**
127: * Get an array holding all registered <code>CastorAppender</code>.
128: *
129: * @return An array holding all registered <code>CastorAppender</code>.
130: */
131: private static synchronized CastorAppender[] getAppenders() {
132: CastorAppender[] appenders = new CastorAppender[_elements
133: .size()];
134: return (CastorAppender[]) _elements.toArray(appenders);
135: }
136:
137: /**
138: * When the program has ended all logger instances are destroyed. To save all data
139: * which are still in the buffer, this method must be called. It saves all data from
140: * all registered <code>CastorAppender</code>.
141: * <br/>
142: * As an alternative <code>org.apache.log4j.LogManager.shutdown()</code> can be
143: * called.
144: */
145: public static void flush() {
146: CastorAppender[] appenders = getAppenders();
147: if (appenders.length > 0) {
148: for (int i = 0; i < appenders.length; i++) {
149: appenders[i].flushBuffer();
150: }
151: }
152: }
153:
154: // -----------------------------------------------------------------------------------
155:
156: /**
157: * Default constructor.
158: */
159: public CastorAppender() {
160: super ();
161: addAppender(this );
162: _database = null;
163: _buffer = new ArrayList(_bufferSize);
164: _removes = new ArrayList(_bufferSize);
165: }
166:
167: /** Closes the appender before disposal. */
168: public void finalize() {
169: close();
170: }
171:
172: /**
173: * Closes the appender, flushing the buffer first then closing the query and database
174: * if it is still open.
175: */
176: public void close() {
177: flushBuffer();
178:
179: if (_database != null) {
180: try {
181: _qry.close();
182: _database.close();
183: } catch (Exception e) {
184: errorHandler.error("Error closing database.", e,
185: ErrorCode.CLOSE_FAILURE);
186: }
187: }
188: this .closed = true;
189: removeAppender(this );
190: }
191:
192: // -----------------------------------------------------------------------------------
193:
194: /**
195: * Adds the event to the buffer. When full the buffer is flushed.
196: *
197: * @param event The event to be logged.
198: */
199: public synchronized void append(final LoggingEvent event) {
200: _buffer.add(event);
201:
202: if (_buffer.size() >= _bufferSize) {
203: flushBuffer();
204: }
205: }
206:
207: /**
208: * Loops through the buffer of <code>LoggingEvents</code> and store them into the
209: * database. If a statement fails the <code>LoggingEvent</code> stays in the buffer!
210: */
211: private synchronized void flushBuffer() {
212: _removes.ensureCapacity(_buffer.size());
213:
214: Database db = getDatabase();
215: try {
216: for (Iterator i = _buffer.iterator(); i.hasNext();) {
217: LoggingEvent logEvent = (LoggingEvent) i.next();
218: execute(logEvent);
219: _removes.add(logEvent);
220: }
221:
222: db.commit();
223:
224: _buffer.removeAll(_removes);
225: _removes.clear();
226: } catch (Exception e) {
227: errorHandler.error("Error flush buffer.", e,
228: ErrorCode.GENERIC_FAILURE);
229: }
230: }
231:
232: /**
233: * Initialize the database and create the query. If the database is already
234: * initialized, only return the database. In both cases a transaction is started.
235: *
236: * @return The initialized database.
237: */
238: private Database getDatabase() {
239: if (_database == null) {
240: try {
241: _database = JDOManager.createInstance(_databaseName)
242: .getDatabase();
243: _database.begin();
244: String oql = "select o from "
245: + LogEntry.class.getName()
246: + " o where o.className = $1 and"
247: + " o.level = $2 and" + " o.message = $3";
248: _qry = _database.getOQLQuery(oql);
249: } catch (Exception e) {
250: errorHandler.error("Error get database.", e,
251: ErrorCode.GENERIC_FAILURE);
252: }
253: } else {
254: try {
255: _database.begin();
256: } catch (Exception e) {
257: errorHandler.error("Cannot begin a transaction.", e,
258: ErrorCode.GENERIC_FAILURE);
259: }
260: }
261: return _database;
262: }
263:
264: /**
265: * Save the given <code>LoggingEvent</code> to the database. If 'duplicateCount' is
266: * enabled, a possible earlier entry is updated. Events with exceptions are stored
267: * ever.
268: *
269: * @param event The <code>LoggingEvent</code> to be saved.
270: */
271: private void execute(final LoggingEvent event) {
272: LogEntry entry;
273: if (event.getMessage() instanceof LogEntry) {
274: entry = (LogEntry) event.getMessage();
275: } else if (event.getMessage() != null) {
276: String message = event.getMessage().toString();
277: message = clipLength(message, _columnWidthMessage);
278: entry = new LogEntry(message);
279: } else {
280: entry = new LogEntry();
281: }
282:
283: String clazz = event.getLoggerName();
284: clazz = clipLength(clazz, _columnWidthClass);
285: entry.setClassName(clazz);
286:
287: String thread = event.getThreadName();
288: thread = clipLength(thread, _columnWidthThread);
289: entry.setThread(thread);
290:
291: entry.setLevel(event.getLevel().toString());
292: entry.setTimestamp(new Date(event.timeStamp));
293:
294: //-----------------------------------------------------------------
295:
296: boolean hasException = (event.getThrowableInformation() != null);
297:
298: if (hasException) {
299: if (_columnWidthStackTrace > 0) {
300: LogExceptionEntry exceptionEntry = new LogExceptionEntry();
301:
302: String temp = "";
303: String[] stackTrace = event.getThrowableStrRep();
304: int stackSize = stackTrace.length;
305: for (int i = 0; i < stackSize; i++) {
306: temp = temp.concat(stackTrace[i] + "\n");
307: }
308:
309: temp = clipLength(temp, _columnWidthStackTrace);
310:
311: exceptionEntry.setStackTrace(temp);
312: exceptionEntry.setEntry(entry);
313: entry.setException(exceptionEntry);
314: }
315: }
316:
317: //-----------------------------------------------------------------
318:
319: try {
320: if (!hasException && _duplicateCount) {
321: _qry.bind(entry.getClassName());
322: _qry.bind(entry.getLevel());
323: _qry.bind(entry.getMessage());
324:
325: QueryResults rst = _qry.execute();
326: if (rst.hasMore()) {
327: LogEntry x = (LogEntry) rst.next();
328: x.setTimestamp(entry.getTimestamp());
329: x.setThread(entry.getThread());
330: x
331: .setCount(new Integer(x.getCount()
332: .intValue() + 1));
333: } else {
334: entry.setCount(new Integer(1));
335: _database.create(entry);
336: }
337: rst.close();
338: } else {
339: entry.setCount(new Integer(1));
340: _database.create(entry);
341: }
342: } catch (Exception e) {
343: errorHandler.error("Cannot save the object.", e,
344: ErrorCode.FLUSH_FAILURE);
345: }
346: }
347:
348: /**
349: * Clip a string to ensure the length. If the string is longer, the rear part is
350: * clipped.
351: *
352: * @param value The string to cut.
353: * @param maxLength The maximum length of this value.
354: * @return The clipped String.
355: */
356: private String clipLength(final String value, final int maxLength) {
357: if (value.length() > maxLength) {
358: return value.substring(0, maxLength);
359: }
360: return value;
361: }
362:
363: /**
364: * CastorAppender don't requires a layout.
365: *
366: * @return <code>true</code> if this appender require a layout, otherwise
367: * <code>false</code>.
368: * */
369: public boolean requiresLayout() {
370: return false;
371: }
372:
373: // -----------------------------------------------------------------------------------
374:
375: /**
376: * Set the size of the buffer.
377: *
378: * @param newBufferSize New size of the buffer.
379: */
380: public void setBufferSize(final int newBufferSize) {
381: _bufferSize = newBufferSize;
382: _buffer.ensureCapacity(_bufferSize);
383: _removes.ensureCapacity(_bufferSize);
384: }
385:
386: /**
387: * Get the size of the buffer.
388: *
389: * @return The size of the buffer.
390: */
391: public int getBufferSize() {
392: return _bufferSize;
393: }
394:
395: /**
396: * Set the name of the database.
397: *
398: * @param name Name of the database.
399: */
400: public void setDatabaseName(final String name) {
401: _databaseName = name;
402: }
403:
404: /**
405: * Get the name of the database.
406: *
407: * @return Name of the database.
408: */
409: public String getDatabaseName() {
410: return _databaseName;
411: }
412:
413: /**
414: * Set the column width for class information.
415: *
416: * @param columWidth The column width for class information.
417: */
418: public void setColumnWidthClass(final int columWidth) {
419: _columnWidthClass = columWidth;
420: }
421:
422: /**
423: * Get the column width for class information.
424: *
425: * @return The column width for class information.
426: */
427: public int getColumnWidthClass() {
428: return _columnWidthClass;
429: }
430:
431: /**
432: * Set the column width for thread information.
433: *
434: * @param columWidth The column width for thread information.
435: */
436: public void setColumnWidthThread(final int columWidth) {
437: _columnWidthThread = columWidth;
438: }
439:
440: /**
441: * Get the column width for tread information.
442: *
443: * @return The column width for thread information.
444: */
445: public int getColumnWidthThread() {
446: return _columnWidthThread;
447: }
448:
449: /**
450: * Set the column width for message information.
451: *
452: * @param columWidth The column width for message information.
453: */
454: public void setColumnWidthMessage(final int columWidth) {
455: _columnWidthMessage = columWidth;
456: }
457:
458: /**
459: * Get the column width for message information.
460: *
461: * @return The column width for message information.
462: */
463: public int getColumnWidthMessage() {
464: return _columnWidthMessage;
465: }
466:
467: /**
468: * Set the column width for stack trace information.
469: *
470: * @param columWidth The column width for stack trace information.
471: */
472: public void setColumnWidthStackTrace(final int columWidth) {
473: _columnWidthStackTrace = columWidth;
474: }
475:
476: /**
477: * Get the column width for stack trace information.
478: *
479: * @return The column width for stack trace information.
480: */
481: public int getColumnWidthStackTrace() {
482: return _columnWidthStackTrace;
483: }
484:
485: /**
486: * Set duplicate count.
487: *
488: * @param duplicateCount Should duplicate count be enabled?
489: */
490: public void setDuplicateCount(final String duplicateCount) {
491: String temp = duplicateCount.toLowerCase();
492: if ("true".equals(temp)) {
493: _duplicateCount = true;
494: } else {
495: _duplicateCount = false;
496: }
497: }
498:
499: /**
500: * Is duplicate count enabled?
501: *
502: * @return <code>true</code> if duplicate count is enabled, otherwise
503: * <code>false</code>.
504: */
505: public String getDuplicateCount() {
506: return new Boolean(_duplicateCount).toString();
507: }
508:
509: // -----------------------------------------------------------------------------------
510: }
|