View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  package org.apache.log4j.jdbc;
    18
    19  import java.sql.Connection;
    20  import java.sql.DriverManager;
    21  import java.sql.SQLException;
    22  import java.sql.Statement;
    23  import java.util.ArrayList;
    24  import java.util.Iterator;
    25
    26  import org.apache.log4j.PatternLayout;
    27  import org.apache.log4j.spi.ErrorCode;
    28  import org.apache.log4j.spi.LoggingEvent;
    29
    30
    31  /**
    32    The JDBCAppender provides for sending log events to a database.
    33
    34   <p><b><font color="#FF2222">WARNING: This version of JDBCAppender
    35   is very likely to be completely replaced in the future. Moreoever,
    36   it does not log exceptions</font></b>.
    37
    38    <p>Each append call adds to an <code>ArrayList</code> buffer.  When
    39    the buffer is filled each log event is placed in a sql statement
    40    (configurable) and executed.
    41
    42    <b>BufferSize</b>, <b>db URL</b>, <b>User</b>, & <b>Password</b> are
    43    configurable options in the standard log4j ways.
    44
    45    <p>The <code>setSql(String sql)</code> sets the SQL statement to be
    46    used for logging -- this statement is sent to a
    47    <code>PatternLayout</code> (either created automaticly by the
    48    appender or added by the user).  Therefore by default all the
    49    conversion patterns in <code>PatternLayout</code> can be used
    50    inside of the statement.  (see the test cases for examples)
    51
    52    <p>Overriding the {@link #getLogStatement} method allows more
    53    explicit control of the statement used for logging.
    54
    55    <p>For use as a base class:
    56
    57      <ul>
    58
    59      <li>Override <code>getConnection()</code> to pass any connection
    60      you want.  Typically this is used to enable application wide
    61      connection pooling.
    62
    63       <li>Override <code>closeConnection(Connection con)</code> -- if
    64       you override getConnection make sure to implement
    65       <code>closeConnection</code> to handle the connection you
    66       generated.  Typically this would return the connection to the
    67       pool it came from.
    68
    69       <li>Override <code>getLogStatement(LoggingEvent event)</code> to
    70       produce specialized or dynamic statements. The default uses the
    71       sql option value.
    72
    73      </ul>
    74
    75      @author Kevin Steppe (<A HREF="mailto:ksteppe@pacbell.net">ksteppe@pacbell.net</A>)
    76
    77  */
    78  public class JDBCAppender extends org.apache.log4j.AppenderSkeleton
    79      implements org.apache.log4j.Appender {
    80
    81    /**
    82     * URL of the DB for default connection handling
    83     */
    84    protected String databaseURL = "jdbc:odbc:myDB";
    85
    86    /**
    87     * User to connect as for default connection handling
    88     */
    89    protected String databaseUser = "me";
    90
    91    /**
    92     * User to use for default connection handling
    93     */
    94    protected String databasePassword = "mypassword";
    95
    96    /**
    97     * Connection used by default.  The connection is opened the first time it
    98     * is needed and then held open until the appender is closed (usually at
    99     * 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     } catch (SQLException e) {
    220        if (stmt != null)
    221 	     stmt.close();
    222        throw e;
    223     }
    224     stmt.close();
    225     closeConnection(con);
    226
    227     //System.out.println("Execute: " + sql);
    228   }
    229
    230
    231   /**
    232    * Override this to return the connection to a pool, or to clean up the
    233    * resource.
    234    *
    235    * The default behavior holds a single connection open until the appender
    236    * is closed (typically when garbage collected).
    237    */
    238   protected void closeConnection(Connection con) {
    239   }
    240
    241   /**
    242    * Override this to link with your connection pooling system.
    243    *
    244    * By default this creates a single connection which is held open
    245    * until the object is garbage collected.
    246    */
    247   protected Connection getConnection() throws SQLException {
    248       if (!DriverManager.getDrivers().hasMoreElements())
    249 	     setDriver("sun.jdbc.odbc.JdbcOdbcDriver");
    250
    251       if (connection == null) {
    252         connection = DriverManager.getConnection(databaseURL, databaseUser,
    253 					databasePassword);
    254       }
    255
    256       return connection;
    257   }
    258
    259   /**
    260    * Closes the appender, flushing the buffer first then closing the default
    261    * connection if it is open.
    262    */
    263   public void close()
    264   {
    265     flushBuffer();
    266
    267     try {
    268       if (connection != null && !connection.isClosed())
    269           connection.close();
    270     } catch (SQLException e) {
    271         errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE);
    272     }
    273     this.closed = true;
    274   }
    275
    276   /**
    277    * loops through the buffer of LoggingEvents, gets a
    278    * sql string from getLogStatement() and sends it to execute().
    279    * Errors are sent to the errorHandler.
    280    *
    281    * If a statement fails the LoggingEvent stays in the buffer!
    282    */
    283   public void flushBuffer() {
    284     //Do the actual logging
    285     removes.ensureCapacity(buffer.size());
    286     for (Iterator i = buffer.iterator(); i.hasNext();) {
    287       try {
    288         LoggingEvent logEvent = (LoggingEvent)i.next();
    289 	    String sql = getLogStatement(logEvent);
    290 	    execute(sql);
    291         removes.add(logEvent);
    292       }
    293       catch (SQLException e) {
    294 	    errorHandler.error("Failed to excute sql", e,
    295 			   ErrorCode.FLUSH_FAILURE);
    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