Return-Path: Delivered-To: apmail-logging-log4j-user-archive@www.apache.org Received: (qmail 76766 invoked from network); 9 Jan 2009 11:52:48 -0000 Received: from hermes.apache.org (HELO mail.apache.org) (140.211.11.2) by minotaur.apache.org with SMTP; 9 Jan 2009 11:52:48 -0000 Received: (qmail 45280 invoked by uid 500); 9 Jan 2009 11:52:46 -0000 Delivered-To: apmail-logging-log4j-user-archive@logging.apache.org Received: (qmail 44983 invoked by uid 500); 9 Jan 2009 11:52:45 -0000 Mailing-List: contact log4j-user-help@logging.apache.org; run by ezmlm Precedence: bulk List-Unsubscribe: List-Help: List-Post: List-Id: "Log4J Users List" Reply-To: "Log4J Users List" Delivered-To: mailing list log4j-user@logging.apache.org Received: (qmail 44972 invoked by uid 99); 9 Jan 2009 11:52:45 -0000 Received: from nike.apache.org (HELO nike.apache.org) (192.87.106.230) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 09 Jan 2009 03:52:44 -0800 X-ASF-Spam-Status: No, hits=2.2 required=10.0 tests=HTML_MESSAGE,SPF_PASS X-Spam-Check-By: apache.org Received-SPF: pass (nike.apache.org: local policy) Received: from [206.190.49.155] (HELO web54505.mail.re2.yahoo.com) (206.190.49.155) by apache.org (qpsmtpd/0.29) with SMTP; Fri, 09 Jan 2009 11:52:34 +0000 Received: (qmail 23648 invoked by uid 60001); 9 Jan 2009 11:52:12 -0000 DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws; s=s1024; d=yahoo.com; h=X-YMail-OSG:Received:X-Mailer:References:Date:From:Subject:To:MIME-Version:Content-Type:Message-ID; b=Ei7qY9EWZOwP9L+By9aD1aFY7SXfAvax41zxKqhFzGoNnAX3KUwFj8/EgQm4bqpXqKHpDlfJ1Hn7qu3er6Ttnvtp0Q8KU3MpkRFavj6tilzAY+dnEMS1GbZQe2ru+4IOSch+Qxvt/Z/SXIC2+Rzp0fZZLn70KZdeJkdVJDp3j28=; X-YMail-OSG: WzJ3Vf8VM1krUVA_Z4RIKuD.TcCrrs7Rg.XIrS8jmXgYx1FTHVpvopLMFMeps6ng5_uouVqZXjn8RJUT_R9rG6VQIO7afEykZs2aWzT4bbifjvt3MOTy8hH5ZXDj7es9lqXxz1rJnm5EhsSBn1._3OPUSgTLWVzE4hiyvoeKICAvKvQ0hRapyx51vhL8sTWRtugvhsCQyjUPmKsqqiib.4aHyuoGkX8- Received: from [62.189.185.102] by web54505.mail.re2.yahoo.com via HTTP; Fri, 09 Jan 2009 03:52:12 PST X-Mailer: YahooMailRC/1155.45 YahooMailWebService/0.7.260.1 References: <4f19b4520901081301p1429d3c0ye2f0434dbccbd705@mail.gmail.com> <06BA3262D918014F9183B66425D5A8D44FF8B99817@no-sv-03.ketech.local> Date: Fri, 9 Jan 2009 03:52:12 -0800 (PST) From: dhallammail-log4j@yahoo.com Subject: Re: Writing to Database To: Log4J Users List MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="0-593879374-1231501932=:22780" Message-ID: <397206.22780.qm@web54505.mail.re2.yahoo.com> X-Virus-Checked: Checked by ClamAV on apache.org --0-593879374-1231501932=:22780 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable I'm using Oracle external tables to do this using a custom Layout implement= ation and an extended RollingFileAppender. There's a background process th= at materializes the data into the database and you can query the external t= able and join onto it the same as any other table - it's just read-only fro= m the DB - so even when it's not materialized you can still query it. I th= ink other DBs have a similar thing as well.=0A=0AAnother option maybe?=0A= =0A=0A=0A=0A________________________________=0AFrom: Michael Erskine =0ATo: Log4J Users List =0ASent: Friday, 9 January, 2009 9:39:12=0ASubject: RE: Writing to Databa= se=0A=0AAswani Kommireddy [mailto:sriaswani@gmail.com]:-=0A> We want to wri= te Logs to database, but not one DB call per logging=0A> event.=0A> Plans a= re like collecting all logging events till the end of each=0A> server reque= st, and commit that to database as a single message(single=0A> record). I m= ean collect all log messages of a request (client - server=0A> client) into= some sort of buffer, and insert all those messages buffer=0A> as a single = message (clob data) into the DB table.=0A=0AI don't know what's stopping yo= u just coding this -- I could write it for you but my standard contract rat= e applies :)=0A=0ASome hints below: with a custom appender, queue up all yo= ur logged events and flush them upon some trigger using a background thread= for db writing...=0A=0Aimport java.beans.PropertyChangeListener;=0Aimport = java.beans.PropertyChangeSupport;=0Aimport java.sql.Connection;=0Aimport ja= va.sql.PreparedStatement;=0Aimport java.sql.SQLException;=0Aimport java.sql= .Statement;=0Aimport java.sql.Timestamp;=0Aimport java.util.Arrays;=0Aimpor= t java.util.List;=0Aimport java.util.concurrent.BlockingDeque;=0Aimport jav= a.util.concurrent.LinkedBlockingDeque;=0A=0Aimport org.apache.commons.lang.= StringUtils;=0Aimport org.apache.commons.lang.math.NumberUtils;=0Aimport or= g.apache.commons.lang.time.DateUtils;=0Aimport org.apache.log4j.AppenderSke= leton;=0Aimport org.apache.log4j.Layout;=0Aimport org.apache.log4j.Level;= =0Aimport org.apache.log4j.helpers.LogLog;=0Aimport org.apache.log4j.spi.Lo= ggingEvent;=0A=0Apublic class CollectingAppender extends AppenderSkeleton {= =0A /**=0A * Time in epoch milliseconds that any CollectingAppender= instance last=0A * failed to connect to its database: initialised to -= 1 which means no=0A * appender has yet tried, 0 means connected OK, and= a positive value is a=0A * timestamp=0A */=0A public static lon= g dbFailureTime =3D -1;=0A /**=0A * Upon failure don't hammer the co= nnection port=0A */=0A private static long dbFailureRetryInterval = =3D 60000;=0A /**=0A * Blocking double-ended queue allowing asynchro= nous processing of event=0A * information by a background thread.=0A = */=0A private BlockingDeque evtDataQueue =3D null;=0A /** = Logging database driver class name */=0A private String dbdriver =3D "co= m.mysql.jdbc.Driver";=0A /** Logging database url - normally contains us= er and password too */=0A private String dburl =3D "jdbc:mysql://localho= st/whatever?user=3Dwhatever&password=3Dwhatever";=0A /** Logging databas= e username - only used if set */=0A private String dbuser =3D null;=0A = /** Logging database password - only used if set */=0A private String = dbpass =3D null;=0A /** The single database connection - initially null = */=0A private Connection dbcon =3D null;=0A /** The database prepared= statement - initially null */=0A private PreparedStatement dbps =3D nul= l;=0A /** The connection can be flagged suspect */=0A private boolean= dbconsuspect =3D false;=0A /** Limited size for logged message text. */= =0A private int maxMessageLength =3D 1200;=0A /** Limited size for lo= gged exception text. */=0A private int maxExceptionLength =3D 1200;=0A = /** Timestamp of last database purge. */=0A /** Database purge stateme= nt - configurable. */=0A private String purgeStatement =3D "delete from = whatever_log "=0A + "WHERE evt_time < DATE_SUB(CURDATE(),INTERVA= L 10 DAY)";=0A /** Time between database purges in hours - defaults to 4= . */=0A private long purgeIntervalHours =3D 4;=0A private long purgeL= ast =3D 0;=0A=0A /** Last logged message property that can be tracked by= observers. */=0A private String lastLoggedMessage =3D "nothing logged!"= ;=0A /** Property change support mechanism for tracking logs */=0A pr= ivate PropertyChangeSupport changes =3D new PropertyChangeSupport(this);=0A= =0A /**=0A * Have a limited length queue for incoming log events. A = zero size means=0A * unlimited.=0A */=0A private int maxQueueSiz= e =3D 300;=0A=0A /**=0A * The database event logging insert statemen= t - must have correct=0A * parameters=0A */=0A private String db= insert =3D "INSERT INTO whatever_log ("=0A + "evt_time, evt_leve= l, evt_loggername, evt_threadname,"=0A + "evt_rendered, evt_exce= ption, evt_layout"=0A + ") VALUES (?, ?, ?, ?, ?, ?, ?)";=0A=0A = /**=0A * Simple query to test a database connection. Should return a= single row=0A * with a single column=0A */=0A private String db= teststring =3D "SELECT VERSION()";=0A=0A /** Background thread to proces= s async queue */=0A private Thread bg_thread;=0A=0A /** Set when shut= down */=0A private boolean shutdown =3D false;=0A /**=0A * A logg= ed event that is queued for writing to the logging database in a=0A * b= ackground thread.=0A *=0A * @author Michael Erskine=0A */=0A = public class EvtData {=0A=0A private String level;=0A privat= e long timeStamp;=0A private String loggerName;=0A private St= ring threadName;=0A private String renderedMessage;=0A privat= e String exception;=0A private String layoutMsg;=0A=0A /**=0A= * @param level=0A * @param timeStamp=0A * @param l= oggerName=0A * @param threadName=0A * @param renderedMessag= e=0A * @param throwableStrRep=0A * @param layoutMsg=0A = */=0A public EvtData(String level, long timeStamp, String logger= Name,=0A String threadName, String renderedMessage,=0A = String[] throwableStrRep, String layoutMsg) {=0A=0A th= is.level =3D level;=0A this.timeStamp =3D timeStamp;=0A = this.loggerName =3D loggerName;=0A this.threadName =3D thread= Name;=0A this.renderedMessage =3D StringUtils.abbreviate(rendere= dMessage,=0A getMaxMessageLength());=0A if (throw= ableStrRep !=3D null) {=0A exception =3D sm.join(Arrays.asLi= st(throwableStrRep));=0A exception =3D StringUtils.abbreviat= e(exception,=0A getMaxExceptionLength());=0A = // Tack on to the layout message...=0A layoutMsg +=3D ex= ception;=0A }=0A this.layoutMsg =3D StringUtils.abbre= viate(layoutMsg,=0A getMaxMessageLength());=0A }=0A = }=0A=0A /*=0A * (non-Javadoc)=0A *=0A * @see org.apache.lo= g4j.AppenderSkeleton#requiresLayout()=0A */=0A public boolean requir= esLayout() {=0A return true;=0A }=0A=0A /*=0A * (non-Javad= oc)=0A *=0A * @see org.apache.log4j.AppenderSkeleton#activateOption= s()=0A */=0A @Override=0A public void activateOptions() {=0A = LogLog.debug("lma activateOptions called!");=0A if (evtDataQueue = !=3D null) {=0A LogLog.warn("lma queue exists already and will b= e discarded ("=0A + evtDataQueue.size() + " items lost)"= );=0A evtDataQueue.clear();=0A evtDataQueue =3D null;= =0A }=0A ensureQueue();=0A }=0A=0A /*=0A * (non-Jav= adoc)=0A *=0A * @see org.apache.log4j.AppenderSkeleton#append(org.a= pache.log4j.spi.LoggingEvent)=0A */=0A @Override=0A protected voi= d append(LoggingEvent ev) {=0A if (shutdown) {=0A LogLog.= error("lma append when shutdown - Main closing?");=0A return;=0A= }=0A=0A // Get the useful elements from the event object...= =0A Level lev =3D ev.getLevel();=0A String level =3D lev =3D= =3D null ? null : lev.toString();=0A // The timestamp is in epoch mi= lliseconds as received=0A // from System.currentTimeMillis() and is = a wall clock time=0A // that tracks system clock changes. It is for = human consumption only!=0A // and should not be used as an interval = measurement...=0A long timeStamp =3D ev.getTimeStamp();=0A St= ring loggerName =3D ev.getLoggerName();=0A String threadName =3D ev.= getThreadName();=0A String renderedMessage =3D ev.getRenderedMessage= ();=0A String[] throwableStrRep =3D ev.getThrowableStrRep();=0A = Layout lay =3D getLayout();=0A String layoutMsg =3D null;=0A = if (lay !=3D null) {=0A layoutMsg =3D lay.format(ev);=0A = }=0A=0A // Add an event data object to the queue to be processed = soon...=0A EvtData ed =3D new EvtData(level, timeStamp, loggerName, = threadName,=0A renderedMessage, throwableStrRep, layoutMsg);=0A = // Bootstrap bg thread...=0A if (bg_thread =3D=3D null)=0A = startBgThread();=0A // Attempt to add to queue - if full th= en there's nothing we can do=0A if (!evtDataQueue.offer(ed))=0A = LogLog.error("lma queue is full - message lost!");=0A }=0A=0A = /**=0A * Write the given event data to the logging database.=0A * <= p>=0A * Performed in background thread.=0A *=0A * @param evtDat= a=0A * Event data.=0A */=0A private void writeDbE= vtData(EvtData evtData) {=0A if (evtData =3D=3D null) {=0A = LogLog.error("lma - null event data");=0A return;=0A }= =0A // Forward logged event to any listeners...=0A setLastLog= gedMessage(evtData.layoutMsg);=0A // Bootstrap db connection if nece= ssary...=0A if (findConnection() =3D=3D null) {=0A LogLog= .debug("lma no connection found");=0A return;=0A }=0A = // Prepare the logging insert statement if necessary...=0A if (d= bps =3D=3D null) {=0A try {=0A dbps =3D dbcon.pre= pareStatement(dbinsert);=0A } catch (SQLException e) {=0A = LogLog.error("lma failed to prepare insert statement", e);=0A = return;=0A }=0A }=0A try {=0A = int col =3D 0;=0A dbps.setTimestamp(++col, new Timestamp(evtDa= ta.timeStamp));=0A dbps.setString(++col, evtData.level);=0A = dbps.setString(++col, evtData.loggerName);=0A dbps.setStr= ing(++col, evtData.threadName);=0A dbps.setString(++col, evtData= .renderedMessage);=0A dbps.setString(++col, evtData.exception);= =0A dbps.setString(++col, evtData.layoutMsg);=0A dbps= .execute();=0A if (evtData.layoutMsg !=3D null) {=0A = // LogLog.debug("lma wrote: " + evtData.layoutMsg);=0A }=0A = } catch (SQLException e) {=0A LogLog.error("lma failed to= execute insert statement", e);=0A dbconsuspect =3D true;=0A = }=0A }=0A=0A /**=0A * Database connection finder. - when not = connected, connect=0A *=0A * @return a possibly usable database con= nection or null if none found.=0A */=0A private Connection findConne= ction() {=0A // Some database activity previously failed on this con= nection:=0A // test the connection to the database with a simple kno= wn working=0A // query and if that fails abandon the connection so t= hat it will=0A // be re-established...=0A if (dbcon !=3D null= && dbconsuspect) {=0A try {=0A List ls= a =3D DbUtils.doquery(dbcon, getDbteststring());=0A String[]= row =3D lsa.get(1);=0A LogLog.debug("lma suspect test resul= t: '" + row[0] + "'");=0A dbconsuspect =3D false;=0A = // continue to use this connection since it works=0A } c= atch (Exception e) {=0A LogLog.error("lma suspect test faile= d", e);=0A }=0A // Still suspect so close...=0A = if (dbconsuspect) {=0A DbUtils.closeQuietly(dbcon, dbp= s, null);=0A dbcon =3D null;=0A dbps =3D null= ;=0A // How about using Connection.isValid(timeout)=0A = }=0A }=0A=0A // A null connection requires us to attemp= t to make a connection...=0A // TODO: could also cope with a config = change here!=0A if (dbcon =3D=3D null) {=0A dbconsuspect = =3D true;=0A=0A // Back off reconnecting when operating in db fa= ilure mode...=0A if (dbFailureTime > 0=0A && = now() < dbFailureTime + dbFailureRetryInterval) {=0A return = dbcon;=0A }=0A LogLog.debug("lma null dbcon - try con= nect");=0A try {=0A dbcon =3D DbUtils.dbConnect(d= bdriver, dburl, dbuser, dbpass);=0A LogLog.debug("lma got co= nnection");=0A dbFailureTime =3D 0;=0A } catch (S= QLException e) {=0A LogLog.error("lma jdbc connect problem -= check db");=0A dbFailureTime =3D now();=0A } cat= ch (ClassNotFoundException e) {=0A LogLog.error("lma jdbc dr= iver problem: ", e);=0A }=0A }=0A=0A return dbcon;= =0A }=0A=0A /**=0A * Get a timestamp that can be used in the same= manner as=0A * {@link System#currentTimeMillis()}.=0A *=0A * @= return a millisecond resolution timestamp=0A */=0A private long now(= ) {=0A return TimeBroker.offsetLaunch();=0A }=0A=0A @Override= =0A public void close() {=0A shutdown =3D true;=0A // Clos= e the database connection=0A DbUtils.closeQuietly(dbcon, dbps, null)= ;=0A dbcon =3D null;=0A dbps =3D null;=0A // Stop the = background thread...=0A stopBgThread();=0A }=0A=0A private voi= d startBgThread() {=0A Runnable r =3D new Runnable() {=0A = @Override=0A public void run() {=0A LogLog.debug= ("lma bgthread starting");=0A while (!shutdown) {=0A = try {=0A=0AOK, here you'll want to hang fire until a signal to = flush is made by whatever means=0A=0A EvtData e =3D = evtDataQueue.take();=0A if (!shutdown) {=0A = writeDbEvtData(e);=0A }=0A = } catch (InterruptedException e) {=0A L= ogLog.debug("lma bgthread interrupted");=0A }=0A = }=0A LogLog.debug("lma bgthread finished");=0A = }=0A };=0A bg_thread =3D new Thread(r, "lma-bg");=0A = bg_thread.setDaemon(true);=0A bg_thread.start();=0A }=0A=0A = private void stopBgThread() {=0A if (bg_thread =3D=3D null)=0A = return;=0A // If already completed, dereference...=0A = if (!bg_thread.isAlive()) {=0A bg_thread =3D null;=0A }= else {=0A=0A // TODO properly interrupt thread when blocking on= queue - how is=0A // this done in other systems?=0A = bg_thread.interrupt();=0A=0A }=0A=0A }=0A=0A private void ensu= reQueue() {=0A // Bootstrap queue...=0A if (evtDataQueue =3D= =3D null) {=0A LogLog.debug("lma creating queue with max " + get= MaxQueueSize());=0A if (getMaxQueueSize() > 0) {=0A = evtDataQueue =3D new LinkedBlockingDeque(=0A = getMaxQueueSize());=0A } else {=0A evtDataQueue = =3D new LinkedBlockingDeque();=0A }=0A }=0A }= =0A=0A /**=0A * Get the size of the Logged Event Queue.=0A *=0A = * @return event queue size or zero if the queue is null=0A */=0A = public int getEvtDataQueueSize() {=0A if (evtDataQueue =3D=3D null)= {=0A return 0;=0A }=0A return evtDataQueue.size()= ;=0A }=0A=0A /**=0A * Delete old log entries using the configured= dbpurge statement.=0A *

=0A * Limited frequency of execution co= ntrolled by a timestamp.=0A */=0A public void purgeExpiredDbEvents()= {=0A // Do nothing if not enough time has passed since previous pur= ge...=0A if (now() < purgeLast=0A + (purgeIntervalHou= rs * DateUtils.MILLIS_PER_HOUR)) {=0A return;=0A }=0A = LogLog.debug("purgeExpiredDbEvents running");=0A=0A // Operate w= ith temporary objects...=0A try {=0A Connection con =3D D= bUtils.dbConnect(dbdriver, dburl, dbuser, dbpass);=0A Statement = stmt =3D con.createStatement();=0A stmt.execute(purgeStatement);= =0A int updateCount =3D stmt.getUpdateCount();=0A Log= Log.debug("purgeExpiredDbEvents rows deleted: " + updateCount);=0A = DbUtils.closeQuietly(con, stmt, null);=0A } catch (SQLException e= ) {=0A LogLog.error("failed:" + e.getMessage(), e);=0A } = catch (ClassNotFoundException e) {=0A LogLog.error("failed:" + e= .getMessage(), e);=0A }=0A // Update the timestamp...=0A = purgeLast =3D now();=0A }=0A=0A public String getPurgeStatement() = {=0A return purgeStatement;=0A }=0A=0A public void setPurgeSta= tement(String st) {=0A this.purgeStatement =3D st;=0A }=0A=0A = public long getPurgeIntervalHours() {=0A return purgeIntervalHours;= =0A }=0A=0A public void setPurgeIntervalHours(long val) {=0A t= his.purgeIntervalHours =3D val;=0A }=0A=0A public void setBuffer(Stri= ng invalue) {=0A int v =3D NumberUtils.toInt(invalue, -1);=0A = if (v < 0) {=0A String errorMsg =3D "lma buffer value '" + inva= lue + "' not valid";=0A LogLog.error(errorMsg);=0A er= rorHandler.error(errorMsg, null, 0);=0A return;=0A }=0A//= buffer_size =3D v;=0A }=0A=0A public void setDbdriver(String dbdrive= r) {=0A this.dbdriver =3D dbdriver;=0A }=0A=0A public String g= etDbdriver() {=0A return dbdriver;=0A }=0A=0A public void setD= burl(String dburl) {=0A this.dburl =3D dburl;=0A }=0A=0A publi= c String getDburl() {=0A return dburl;=0A }=0A=0A public void = setDbuser(String dbuser) {=0A this.dbuser =3D dbuser;=0A }=0A=0A = public String getDbuser() {=0A return dbuser;=0A }=0A=0A pu= blic void setDbpass(String dbpass) {=0A this.dbpass =3D dbpass;=0A = }=0A=0A public String getDbpass() {=0A return dbpass;=0A }= =0A=0A public void setDbteststring(String dbteststring) {=0A this= .dbteststring =3D dbteststring;=0A }=0A=0A public String getDbteststr= ing() {=0A return dbteststring;=0A }=0A=0A public void setDbin= sert(String dbinsert) {=0A this.dbinsert =3D dbinsert;=0A }=0A=0A= public String getDbinsert() {=0A return dbinsert;=0A }=0A=0A = public void setMaxMessageLength(int maxMessageLength) {=0A int v = =3D maxMessageLength;=0A if (v < 6) {=0A LogLog.error("lm= a maxMessageLength must be > 5");=0A v =3D 6;=0A }=0A = this.maxMessageLength =3D v;=0A }=0A=0A public int getMaxMessageL= ength() {=0A return maxMessageLength;=0A }=0A=0A public void s= etMaxExceptionLength(int maxExceptionLength) {=0A int v =3D maxExcep= tionLength;=0A if (v < 6) {=0A LogLog.error("lma maxExcep= tionLength must be > 5");=0A v =3D 6;=0A }=0A this= .maxExceptionLength =3D v;=0A }=0A=0A public int getMaxExceptionLengt= h() {=0A return maxExceptionLength;=0A }=0A=0A public void set= MaxQueueSize(int maxQueueSize) {=0A this.maxQueueSize =3D maxQueueSi= ze;=0A }=0A=0A public int getMaxQueueSize() {=0A return maxQue= ueSize;=0A }=0A=0A /**=0A * Gets the last logged message.=0A = *=0A * @return the last logged message=0A */=0A public String ge= tLastLoggedMessage() {=0A return lastLoggedMessage;=0A }=0A=0A = /**=0A * Sets the last logged message.=0A *=0A * @param lastLo= ggedMessage=0A * the new last logged message=0A */= =0A public void setLastLoggedMessage(String lastLoggedMessage) {=0A = // Avoid firing off noise when nothing is really happening - this is=0A = // generally good practice for PropertyChangeSupport...=0A if= (this.lastLoggedMessage =3D=3D null && lastLoggedMessage =3D=3D null) {=0A= return;=0A }=0A changes.firePropertyChange("lastL= oggedMessage", this.lastLoggedMessage,=0A this.lastLoggedMessage= =3D lastLoggedMessage);=0A }=0A=0A /**=0A * Adds a listener to r= eceive last logged message changes.=0A *=0A * @param propertyName= =0A * the property to listen on=0A * @param listener= =0A * the listener=0A */=0A public void addEvents= Listener(String propertyName,=0A PropertyChangeListener listener= ) {=0A changes.addPropertyChangeListener("lastLoggedMessage", listen= er);=0A }=0A=0A}=0A=0A...of course there's a lot more to it - this is (m= ost of) a generic asynchronous JDBC appender with a fault-tolerant DB conne= ction (pooling would be preferred) which serves me very well. When I get ro= und to it I'll publish my Log4j extensions under a suitable license.=0A=0AR= egards,=0AMichael Erskine.=0A=0A=0A----------------------------------------= -----------------------------=0ATo unsubscribe, e-mail: log4j-user-unsubscr= ibe@logging.apache.org=0AFor additional commands, e-mail: log4j-user-help@l= ogging.apache.org=0A=0A=0A --0-593879374-1231501932=:22780--