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 org.apache.log4j.spi.*;
    20  import org.apache.log4j.PatternLayout;
    21
    22  import java.util.ArrayList;
    23  import java.util.Iterator;
    24
    25  import java.sql.DriverManager;
    26  import java.sql.Connection;
    27  import java.sql.Statement;
    28  import java.sql.SQLException;
    29
    30
    31  /***
    32    <p><b><font color="#FF2222">WARNING: This version of JDBCAppender
    33    is very likely to be completely replaced in the future. Moreoever,
    34    it does not log exceptions</font></b>.
    35
    36    The JDBCAppender provides for sending log events to a database.
    37
    38
    39    <p>Each append call adds to an <code>ArrayList</code> buffer.  When
    40    the buffer is filled each log event is placed in a sql statement
    41    (configurable) and executed.
    42
    43    <b>BufferSize</b>, <b>db URL</b>, <b>User</b>, & <b>Password</b> are
    44    configurable options in the standard log4j ways.
    45
    46    <p>The <code>setSql(String sql)</code> sets the SQL statement to be
    47    used for logging -- this statement is sent to a
    48    <code>PatternLayout</code> (either created automaticly by the
    49    appender or added by the user).  Therefore by default all the
    50    conversion patterns in <code>PatternLayout</code> can be used
    51    inside of the statement.  (see the test cases for examples)
    52
    53    <p>Overriding the {@link #getLogStatement} method allows more
    54    explicit control of the statement used for logging.
    55
    56    <p>For use as a base class:
    57
    58      <ul>
    59
    60      <li>Override <code>getConnection()</code> to pass any connection
    61      you want.  Typically this is used to enable application wide
    62      connection pooling.
    63
    64       <li>Override <code>closeConnection(Connection con)</code> -- if
    65       you override getConnection make sure to implement
    66       <code>closeConnection</code> to handle the connection you
    67       generated.  Typically this would return the connection to the
    68       pool it came from.
    69
    70       <li>Override <code>getLogStatement(LoggingEvent event)</code> to
    71       produce specialized or dynamic statements. The default uses the
    72       sql option value.
    73
    74      </ul>
    75
    76      @author Kevin Steppe (<A HREF="mailto:ksteppe@pacbell.net">ksteppe@pacbell.net</A>)
    77
    78  */
    79  public class JDBCAppender extends org.apache.log4j.AppenderSkeleton
    80      implements org.apache.log4j.Appender {
    81
    82    /***
    83     * URL of the DB for default connection handling
    84     */
    85    protected String databaseURL = "jdbc:odbc:myDB";
    86
    87    /***
    88     * User to connect as for default connection handling
    89     */
    90    protected String databaseUser = "me";
    91
    92    /***
    93     * User to use for default connection handling
    94     */
    95    protected String databasePassword = "mypassword";
    96
    97    /***
    98     * Connection used by default.  The connection is opened the first time it
    99     * is needed and then held open until the appender is closed (usually at
    100    * garbage collection).  This behavior is best modified by creating a
    101    * sub-class and overriding the <code>getConnection</code> and
    102    * <code>closeConnection</code> methods.
    103    */
    104   protected Connection connection = null;
    105
    106   /***
    107    * Stores the string given to the pattern layout for conversion into a SQL
    108    * statement, eg: insert into LogTable (Thread, Class, Message) values
    109    * ("%t", "%c", "%m").
    110    *
    111    * Be careful of quotes in your messages!
    112    *
    113    * Also see PatternLayout.
    114    */
    115   protected String sqlStatement = "";
    116
    117   /***
    118    * size of LoggingEvent buffer before writting to the database.
    119    * Default is 1.
    120    */
    121   protected int bufferSize = 1;
    122
    123   /***
    124    * ArrayList holding the buffer of Logging Events.
    125    */
    126   protected ArrayList buffer;
    127
    128   /***
    129    * Helper object for clearing out the buffer
    130    */
    131   protected ArrayList removes;
    132
    133   public JDBCAppender() {
    134     super();
    135     buffer = new ArrayList(bufferSize);
    136     removes = new ArrayList(bufferSize);
    137   }
    138
    139   /***
    140    * Adds the event to the buffer.  When full the buffer is flushed.
    141    */
    142   public void append(LoggingEvent event) {
    143     buffer.add(event);
    144
    145     if (buffer.size() >= bufferSize)
    146       flushBuffer();
    147   }
    148
    149   /***
    150    * By default getLogStatement sends the event to the required Layout object.
    151    * The layout will format the given pattern into a workable SQL string.
    152    *
    153    * Overriding this provides direct access to the LoggingEvent
    154    * when constructing the logging statement.
    155    *
    156    */
    157   protected String getLogStatement(LoggingEvent event) {
    158     return getLayout().format(event);
    159   }
    160
    161   /***
    162    *
    163    * Override this to provide an alertnate method of getting
    164    * connections (such as caching).  One method to fix this is to open
    165    * connections at the start of flushBuffer() and close them at the
    166    * end.  I use a connection pool outside of JDBCAppender which is
    167    * accessed in an override of this method.
    168    * */
    169   protected void execute(String sql) throws SQLException {
    170
    171     Connection con = null;
    172     Statement stmt = null;
    173
    174     try {
    175         con = getConnection();
    176
    177         stmt = con.createStatement();
    178         stmt.executeUpdate(sql);
    179     } catch (SQLException e) {
    180        if (stmt != null)
    181 	     stmt.close();
    182        throw e;
    183     }
    184     stmt.close();
    185     closeConnection(con);
    186
    187     //System.out.println("Execute: " + sql);
    188   }
    189
    190
    191   /***
    192    * Override this to return the connection to a pool, or to clean up the
    193    * resource.
    194    *
    195    * The default behavior holds a single connection open until the appender
    196    * is closed (typically when garbage collected).
    197    */
    198   protected void closeConnection(Connection con) {
    199   }
    200
    201   /***
    202    * Override this to link with your connection pooling system.
    203    *
    204    * By default this creates a single connection which is held open
    205    * until the object is garbage collected.
    206    */
    207   protected Connection getConnection() throws SQLException {
    208       if (!DriverManager.getDrivers().hasMoreElements())
    209 	     setDriver("sun.jdbc.odbc.JdbcOdbcDriver");
    210
    211       if (connection == null) {
    212         connection = DriverManager.getConnection(databaseURL, databaseUser,
    213 					databasePassword);
    214       }
    215
    216       return connection;
    217   }
    218
    219   /***
    220    * Closes the appender, flushing the buffer first then closing the default
    221    * connection if it is open.
    222    */
    223   public void close()
    224   {
    225     flushBuffer();
    226
    227     try {
    228       if (connection != null && !connection.isClosed())
    229           connection.close();
    230     } catch (SQLException e) {
    231         errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE);
    232     }
    233     this.closed = true;
    234   }
    235
    236   /***
    237    * loops through the buffer of LoggingEvents, gets a
    238    * sql string from getLogStatement() and sends it to execute().
    239    * Errors are sent to the errorHandler.
    240    *
    241    * If a statement fails the LoggingEvent stays in the buffer!
    242    */
    243   public void flushBuffer() {
    244     //Do the actual logging
    245     removes.ensureCapacity(buffer.size());
    246     for (Iterator i = buffer.iterator(); i.hasNext();) {
    247       try {
    248         LoggingEvent logEvent = (LoggingEvent)i.next();
    249 	    String sql = getLogStatement(logEvent);
    250 	    execute(sql);
    251         removes.add(logEvent);
    252       }
    253       catch (SQLException e) {
    254 	    errorHandler.error("Failed to excute sql", e,
    255 			   ErrorCode.FLUSH_FAILURE);
    256       }
    257     }
    258
    259     // remove from the buffer any events that were reported
    260     buffer.removeAll(removes);
    261
    262     // clear the buffer of reported events
    263     removes.clear();
    264   }
    265
    266
    267   /*** closes the appender before disposal */
    268   public void finalize() {
    269     close();
    270   }
    271
    272
    273   /***
    274    * JDBCAppender requires a layout.
    275    * */
    276   public boolean requiresLayout() {
    277     return true;
    278   }
    279
    280
    281   /***
    282    *
    283    */
    284   public void setSql(String s) {
    285     sqlStatement = s;
    286     if (getLayout() == null) {
    287         this.setLayout(new PatternLayout(s));
    288     }
    289     else {
    290         ((PatternLayout)getLayout()).setConversionPattern(s);
    291     }
    292   }
    293
    294
    295   /***
    296    * Returns pre-formated statement eg: insert into LogTable (msg) values ("%m")
    297    */
    298   public String getSql() {
    299     return sqlStatement;
    300   }
    301
    302
    303   public void setUser(String user) {
    304     databaseUser = user;
    305   }
    306
    307
    308   public void setURL(String url) {
    309     databaseURL = url;
    310   }
    311
    312
    313   public void setPassword(String password) {
    314     databasePassword = password;
    315   }
    316
    317
    318   public void setBufferSize(int newBufferSize) {
    319     bufferSize = newBufferSize;
    320     buffer.ensureCapacity(bufferSize);
    321     removes.ensureCapacity(bufferSize);
    322   }
    323
    324
    325   public String getUser() {
    326     return databaseUser;
    327   }
    328
    329
    330   public String getURL() {
    331     return databaseURL;
    332   }
    333
    334
    335   public String getPassword() {
    336     return databasePassword;
    337   }
    338
    339
    340   public int getBufferSize() {
    341     return bufferSize;
    342   }
    343
    344
    345   /***
    346    * Ensures that the given driver class has been loaded for sql connection
    347    * creation.
    348    */
    349   public void setDriver(String driverClass) {
    350     try {
    351       Class.forName(driverClass);
    352     } catch (Exception e) {
    353       errorHandler.error("Failed to load driver", e,
    354 			 ErrorCode.GENERIC_FAILURE);
    355     }
    356   }
    357 }
    358