001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 * 
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 * 
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.log4j.jdbc;
018
019import java.sql.Connection;
020import java.sql.DriverManager;
021import java.sql.SQLException;
022import java.sql.Statement;
023import java.util.ArrayList;
024import java.util.Iterator;
025
026import org.apache.log4j.PatternLayout;
027import org.apache.log4j.spi.ErrorCode;
028import org.apache.log4j.spi.LoggingEvent;
029
030
031/**
032  The JDBCAppender provides for sending log events to a database.
033  
034 <p><b><font color="#FF2222">WARNING: This version of JDBCAppender
035 is very likely to be completely replaced in the future. Moreoever,
036 it does not log exceptions</font></b>.
037
038  <p>Each append call adds to an <code>ArrayList</code> buffer.  When
039  the buffer is filled each log event is placed in a sql statement
040  (configurable) and executed.
041
042  <b>BufferSize</b>, <b>db URL</b>, <b>User</b>, & <b>Password</b> are
043  configurable options in the standard log4j ways.
044
045  <p>The <code>setSql(String sql)</code> sets the SQL statement to be
046  used for logging -- this statement is sent to a
047  <code>PatternLayout</code> (either created automaticly by the
048  appender or added by the user).  Therefore by default all the
049  conversion patterns in <code>PatternLayout</code> can be used
050  inside of the statement.  (see the test cases for examples)
051
052  <p>Overriding the {@link #getLogStatement} method allows more
053  explicit control of the statement used for logging.
054
055  <p>For use as a base class:
056
057    <ul>
058
059    <li>Override <code>getConnection()</code> to pass any connection
060    you want.  Typically this is used to enable application wide
061    connection pooling.
062
063     <li>Override <code>closeConnection(Connection con)</code> -- if
064     you override getConnection make sure to implement
065     <code>closeConnection</code> to handle the connection you
066     generated.  Typically this would return the connection to the
067     pool it came from.
068
069     <li>Override <code>getLogStatement(LoggingEvent event)</code> to
070     produce specialized or dynamic statements. The default uses the
071     sql option value.
072
073    </ul>
074
075    @author Kevin Steppe (<A HREF="mailto:ksteppe@pacbell.net">ksteppe@pacbell.net</A>)
076
077*/
078public class JDBCAppender extends org.apache.log4j.AppenderSkeleton
079    implements org.apache.log4j.Appender {
080
081  /**
082   * URL of the DB for default connection handling
083   */
084  protected String databaseURL = "jdbc:odbc:myDB";
085
086  /**
087   * User to connect as for default connection handling
088   */
089  protected String databaseUser = "me";
090
091  /**
092   * User to use for default connection handling
093   */
094  protected String databasePassword = "mypassword";
095
096  /**
097   * Connection used by default.  The connection is opened the first time it
098   * is needed and then held open until the appender is closed (usually at
099   * garbage collection).  This behavior is best modified by creating a
100   * sub-class and overriding the <code>getConnection</code> and
101   * <code>closeConnection</code> methods.
102   */
103  protected Connection connection = null;
104
105  /**
106   * Stores the string given to the pattern layout for conversion into a SQL
107   * statement, eg: insert into LogTable (Thread, Class, Message) values
108   * ("%t", "%c", "%m").
109   *
110   * Be careful of quotes in your messages!
111   *
112   * Also see PatternLayout.
113   */
114  protected String sqlStatement = "";
115
116  /**
117   * size of LoggingEvent buffer before writting to the database.
118   * Default is 1.
119   */
120  protected int bufferSize = 1;
121
122  /**
123   * ArrayList holding the buffer of Logging Events.
124   */
125  protected ArrayList buffer;
126
127  /**
128   * Helper object for clearing out the buffer
129   */
130  protected ArrayList removes;
131  
132  private boolean locationInfo = false;
133
134  public JDBCAppender() {
135    super();
136    buffer = new ArrayList(bufferSize);
137    removes = new ArrayList(bufferSize);
138  }
139
140  /**
141   * Gets whether the location of the logging request call
142   * should be captured.
143   *
144   * @since 1.2.16
145   * @return the current value of the <b>LocationInfo</b> option.
146   */
147  public boolean getLocationInfo() {
148    return locationInfo;
149  }
150  
151  /**
152   * The <b>LocationInfo</b> option takes a boolean value. By default, it is
153   * set to false which means there will be no effort to extract the location
154   * information related to the event. As a result, the event that will be
155   * ultimately logged will likely to contain the wrong location information
156   * (if present in the log format).
157   * <p/>
158   * <p/>
159   * Location information extraction is comparatively very slow and should be
160   * avoided unless performance is not a concern.
161   * </p>
162   * @since 1.2.16
163   * @param flag true if location information should be extracted.
164   */
165  public void setLocationInfo(final boolean flag) {
166    locationInfo = flag;
167  }
168  
169
170  /**
171   * Adds the event to the buffer.  When full the buffer is flushed.
172   */
173  public void append(LoggingEvent event) {
174    event.getNDC();
175    event.getThreadName();
176    // Get a copy of this thread's MDC.
177    event.getMDCCopy();
178    if (locationInfo) {
179      event.getLocationInformation();
180    }
181    event.getRenderedMessage();
182    event.getThrowableStrRep();
183    buffer.add(event);
184
185    if (buffer.size() >= bufferSize)
186      flushBuffer();
187  }
188
189  /**
190   * By default getLogStatement sends the event to the required Layout object.
191   * The layout will format the given pattern into a workable SQL string.
192   *
193   * Overriding this provides direct access to the LoggingEvent
194   * when constructing the logging statement.
195   *
196   */
197  protected String getLogStatement(LoggingEvent event) {
198    return getLayout().format(event);
199  }
200
201  /**
202   *
203   * Override this to provide an alertnate method of getting
204   * connections (such as caching).  One method to fix this is to open
205   * connections at the start of flushBuffer() and close them at the
206   * end.  I use a connection pool outside of JDBCAppender which is
207   * accessed in an override of this method.
208   * */
209  protected void execute(String sql) throws SQLException {
210
211    Connection con = null;
212    Statement stmt = null;
213
214    try {
215        con = getConnection();
216
217        stmt = con.createStatement();
218        stmt.executeUpdate(sql);
219    } finally {
220        if(stmt != null) {
221            stmt.close();
222        }
223        closeConnection(con);
224    }
225
226    //System.out.println("Execute: " + sql);
227  }
228
229
230  /**
231   * Override this to return the connection to a pool, or to clean up the
232   * resource.
233   *
234   * The default behavior holds a single connection open until the appender
235   * is closed (typically when garbage collected).
236   */
237  protected void closeConnection(Connection con) {
238  }
239
240  /**
241   * Override this to link with your connection pooling system.
242   *
243   * By default this creates a single connection which is held open
244   * until the object is garbage collected.
245   */
246  protected Connection getConnection() throws SQLException {
247      if (!DriverManager.getDrivers().hasMoreElements())
248             setDriver("sun.jdbc.odbc.JdbcOdbcDriver");
249
250      if (connection == null) {
251        connection = DriverManager.getConnection(databaseURL, databaseUser,
252                                        databasePassword);
253      }
254
255      return connection;
256  }
257
258  /**
259   * Closes the appender, flushing the buffer first then closing the default
260   * connection if it is open.
261   */
262  public void close()
263  {
264    flushBuffer();
265
266    try {
267      if (connection != null && !connection.isClosed())
268          connection.close();
269    } catch (SQLException e) {
270        errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE);
271    }
272    this.closed = true;
273  }
274
275  /**
276   * loops through the buffer of LoggingEvents, gets a
277   * sql string from getLogStatement() and sends it to execute().
278   * Errors are sent to the errorHandler.
279   *
280   * If a statement fails the LoggingEvent stays in the buffer!
281   */
282  public void flushBuffer() {
283    //Do the actual logging
284    removes.ensureCapacity(buffer.size());
285    for (Iterator i = buffer.iterator(); i.hasNext();) {
286      LoggingEvent logEvent = (LoggingEvent)i.next();
287      try {
288            String sql = getLogStatement(logEvent);
289            execute(sql);
290      }
291      catch (SQLException e) {
292            errorHandler.error("Failed to excute sql", e,
293                           ErrorCode.FLUSH_FAILURE);
294      } finally {
295        removes.add(logEvent);
296      }
297    }
298    
299    // remove from the buffer any events that were reported
300    buffer.removeAll(removes);
301    
302    // clear the buffer of reported events
303    removes.clear();
304  }
305
306
307  /** closes the appender before disposal */
308  public void finalize() {
309    close();
310  }
311
312
313  /**
314   * JDBCAppender requires a layout.
315   * */
316  public boolean requiresLayout() {
317    return true;
318  }
319
320
321  /**
322   *
323   */
324  public void setSql(String s) {
325    sqlStatement = s;
326    if (getLayout() == null) {
327        this.setLayout(new PatternLayout(s));
328    }
329    else {
330        ((PatternLayout)getLayout()).setConversionPattern(s);
331    }
332  }
333
334
335  /**
336   * Returns pre-formated statement eg: insert into LogTable (msg) values ("%m")
337   */
338  public String getSql() {
339    return sqlStatement;
340  }
341
342
343  public void setUser(String user) {
344    databaseUser = user;
345  }
346
347
348  public void setURL(String url) {
349    databaseURL = url;
350  }
351
352
353  public void setPassword(String password) {
354    databasePassword = password;
355  }
356
357
358  public void setBufferSize(int newBufferSize) {
359    bufferSize = newBufferSize;
360    buffer.ensureCapacity(bufferSize);
361    removes.ensureCapacity(bufferSize);
362  }
363
364
365  public String getUser() {
366    return databaseUser;
367  }
368
369
370  public String getURL() {
371    return databaseURL;
372  }
373
374
375  public String getPassword() {
376    return databasePassword;
377  }
378
379
380  public int getBufferSize() {
381    return bufferSize;
382  }
383
384
385  /**
386   * Ensures that the given driver class has been loaded for sql connection
387   * creation.
388   */
389  public void setDriver(String driverClass) {
390    try {
391      Class.forName(driverClass);
392    } catch (Exception e) {
393      errorHandler.error("Failed to load driver", e,
394                         ErrorCode.GENERIC_FAILURE);
395    }
396  }
397}
398