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
    18  package org.apache.log4j.db;
    19
    20  import org.apache.log4j.AppenderSkeleton;
    21  import org.apache.log4j.db.dialect.SQLDialect;
    22  import org.apache.log4j.db.dialect.Util;
    23  import org.apache.log4j.helpers.LogLog;
    24  import org.apache.log4j.spi.LocationInfo;
    25  import org.apache.log4j.spi.LoggingEvent;
    26  import org.apache.log4j.xml.DOMConfigurator;
    27  import org.apache.log4j.xml.UnrecognizedElementHandler;
    28  import org.w3c.dom.Element;
    29
    30  import java.lang.reflect.InvocationTargetException;
    31  import java.lang.reflect.Method;
    32  import java.sql.Connection;
    33  import java.sql.PreparedStatement;
    34  import java.sql.ResultSet;
    35  import java.sql.SQLException;
    36  import java.sql.Statement;
    37  import java.util.Iterator;
    38  import java.util.Properties;
    39  import java.util.Set;
    40
    41
    42  /**
    43   * The DBAppender inserts loggin events into three database tables in a format
    44   * independent of the Java programming language. The three tables that
    45   * DBAppender inserts to must exists before DBAppender can be used. These tables
    46   * may be created with the help of SQL scripts found in the
    47   * <em>src/java/org/apache/log4j/db/dialect</em> directory. There is a
    48   * specific script for each of the most popular database systems. If the script
    49   * for your particular type of database system is missing, it should be quite
    50   * easy to write one, taking example on the already existing scripts. If you
    51   * send them to us, we will gladly include missing scripts in future releases.
    52   *
    53   * <p>
    54   * If the JDBC driver you are using supports the
    55   * {@link java.sql.Statement#getGeneratedKeys}method introduced in JDBC 3.0
    56   * specification, then you are all set. Otherwise, there must be an
    57   * {@link SQLDialect}appropriate for your database system. Currently, we have
    58   * dialects for PostgreSQL, MySQL, Oracle and MsSQL. As mentioed previously, an
    59   * SQLDialect is required only if the JDBC driver for your database system does
    60   * not support the {@link java.sql.Statement#getGeneratedKeys getGeneratedKeys}
    61   * method.
    62   * </p>
    63   *
    64   * <table border="1" cellpadding="4">
    65   * <caption>supported dialects</caption>
    66   * <tr>
    67   * <th>RDBMS</th>
    68   * <th>supports <br><code>getGeneratedKeys()</code> method</th>
    69   * <th>specific <br>SQLDialect support</th>
    70   * <tr>
    71   * <tr>
    72   * <td>PostgreSQL</td>
    73   * <td align="center">NO</td>
    74   * <td>present and used</td>
    75   * <tr>
    76   * <tr>
    77   * <td>MySQL</td>
    78   * <td align="center">YES</td>
    79   * <td>present, but not actually needed or used</td>
    80   * <tr>
    81   * <tr>
    82   * <td>Oracle</td>
    83   * <td align="center">YES</td>
    84   * <td>present, but not actually needed or used</td>
    85   * <tr>
    86   * <tr>
    87   * <td>DB2</td>
    88   * <td align="center">YES</td>
    89   * <td>not present, and not needed or used</td>
    90   * <tr>
    91   * <tr>
    92   * <td>MsSQL</td>
    93   * <td align="center">YES</td>
    94   * <td>not present, and not needed or used</td>
    95   * <tr>
    96   * <tr>
    97   *   <td>HSQL</td>
    98   *    <td align="center">NO</td>
    99   *    <td>present and used</td>
    100  * <tr>
    101  *
    102  * </table>
    103  * <p>
    104  * <b>Performance: </b> Experiments show that writing a single event into the
    105  * database takes approximately 50 milliseconds, on a "standard" PC. If pooled
    106  * connections are used, this figure drops to under 10 milliseconds. Note that
    107  * most JDBC drivers already ship with connection pooling support.
    108  * </p>
    109  *
    110  *
    111  *
    112  * <p>
    113  * <b>Configuration </b> DBAppender can be configured programmatically, or using
    114  * {@link org.apache.log4j.xml.DOMConfigurator JoranConfigurator}. Example
    115  * scripts can be found in the <em>tests/input/db</em> directory.
    116  *
    117  * @author Ceki G&uuml;lc&uuml;
    118  * @author Ray DeCampo
    119  */
    120 public class DBAppender extends AppenderSkeleton implements UnrecognizedElementHandler {
    121   static final String insertPropertiesSQL =
    122     "INSERT INTO  logging_event_property (event_id, mapped_key, mapped_value) VALUES (?, ?, ?)";
    123   static final String insertExceptionSQL =
    124     "INSERT INTO  logging_event_exception (event_id, i, trace_line) VALUES (?, ?, ?)";
    125   static final String insertSQL;
    126   private static final Method GET_GENERATED_KEYS_METHOD;
    127
    128
    129   static {
    130     StringBuffer sql = new StringBuffer();
    131     sql.append("INSERT INTO logging_event (");
    132     sql.append("sequence_number, ");
    133     sql.append("timestamp, ");
    134     sql.append("rendered_message, ");
    135     sql.append("logger_name, ");
    136     sql.append("level_string, ");
    137     sql.append("ndc, ");
    138     sql.append("thread_name, ");
    139     sql.append("reference_flag, ");
    140     sql.append("caller_filename, ");
    141     sql.append("caller_class, ");
    142     sql.append("caller_method, ");
    143     sql.append("caller_line) ");
    144     sql.append(" VALUES (?, ?, ? ,?, ?, ?, ?, ?, ?, ?, ?, ?)");
    145     insertSQL = sql.toString();
    146     //
    147     //   PreparedStatement.getGeneratedKeys added in JDK 1.4
    148     //
    149     Method getGeneratedKeysMethod;
    150     try {
    151         getGeneratedKeysMethod = PreparedStatement.class.getMethod("getGeneratedKeys", null);
    152     } catch(Exception ex) {
    153         getGeneratedKeysMethod = null;
    154     }
    155     GET_GENERATED_KEYS_METHOD = getGeneratedKeysMethod;
    156   }
    157
    158   ConnectionSource connectionSource;
    159   boolean cnxSupportsGetGeneratedKeys = false;
    160   boolean cnxSupportsBatchUpdates = false;
    161   SQLDialect sqlDialect;
    162   boolean locationInfo = false;
    163
    164
    165   public DBAppender() {
    166       super(false);
    167   }
    168
    169   public void activateOptions() {
    170     LogLog.debug("DBAppender.activateOptions called");
    171
    172     if (connectionSource == null) {
    173       throw new IllegalStateException(
    174         "DBAppender cannot function without a connection source");
    175     }
    176
    177     sqlDialect = Util.getDialectFromCode(connectionSource.getSQLDialectCode());
    178     if (GET_GENERATED_KEYS_METHOD != null) {
    179         cnxSupportsGetGeneratedKeys = connectionSource.supportsGetGeneratedKeys();
    180     } else {
    181         cnxSupportsGetGeneratedKeys = false;
    182     }
    183     cnxSupportsBatchUpdates = connectionSource.supportsBatchUpdates();
    184     if (!cnxSupportsGetGeneratedKeys && (sqlDialect == null)) {
    185       throw new IllegalStateException(
    186         "DBAppender cannot function if the JDBC driver does not support getGeneratedKeys method *and* without a specific SQL dialect");
    187     }
    188
    189     // all nice and dandy on the eastern front
    190     super.activateOptions();
    191   }
    192
    193   /**
    194    * @return Returns the connectionSource.
    195    */
    196   public ConnectionSource getConnectionSource() {
    197     return connectionSource;
    198   }
    199
    200   /**
    201    * @param connectionSource
    202    *          The connectionSource to set.
    203    */
    204   public void setConnectionSource(ConnectionSource connectionSource) {
    205     LogLog.debug("setConnectionSource called for DBAppender");
    206     this.connectionSource = connectionSource;
    207   }
    208
    209   protected void append(LoggingEvent event) {
    210       Connection connection = null;
    211       try {
    212           connection = connectionSource.getConnection();
    213           connection.setAutoCommit(false);
    214
    215           PreparedStatement insertStatement;
    216           if (cnxSupportsGetGeneratedKeys) {
    217         	  insertStatement = connection.prepareStatement(insertSQL, Statement.RETURN_GENERATED_KEYS);
    218           } else {
    219               insertStatement = connection.prepareStatement(insertSQL);
    220           }
    221
    222 /*          insertStatement.setLong(1, event.getSequenceNumber());*/
    223 		  insertStatement.setLong(1, 0);
    224
    225           insertStatement.setLong(2, event.getTimeStamp());
    226           insertStatement.setString(3, event.getRenderedMessage());
    227           insertStatement.setString(4, event.getLoggerName());
    228           insertStatement.setString(5, event.getLevel().toString());
    229           insertStatement.setString(6, event.getNDC());
    230           insertStatement.setString(7, event.getThreadName());
    231           insertStatement.setShort(8, DBHelper.computeReferenceMask(event));
    232
    233           LocationInfo li;
    234
    235           if (event.locationInformationExists() || locationInfo) {
    236               li = event.getLocationInformation();
    237           } else {
    238               li = LocationInfo.NA_LOCATION_INFO;
    239           }
    240
    241           insertStatement.setString(9, li.getFileName());
    242           insertStatement.setString(10, li.getClassName());
    243           insertStatement.setString(11, li.getMethodName());
    244           insertStatement.setString(12, li.getLineNumber());
    245
    246           int updateCount = insertStatement.executeUpdate();
    247           if (updateCount != 1) {
    248               LogLog.warn("Failed to insert loggingEvent");
    249           }
    250
    251           ResultSet rs = null;
    252           Statement idStatement = null;
    253           boolean gotGeneratedKeys = false;
    254           if (cnxSupportsGetGeneratedKeys) {
    255               try {
    256                   rs = (ResultSet) GET_GENERATED_KEYS_METHOD.invoke(insertStatement, null);
    257                   gotGeneratedKeys = true;
    258               } catch(InvocationTargetException ex) {
    259                   Throwable target = ex.getTargetException();
    260                   if (target instanceof SQLException) {
    261                       throw (SQLException) target;
    262                   }
    263                   throw ex;
    264               } catch(IllegalAccessException ex) {
    265                   LogLog.warn("IllegalAccessException invoking PreparedStatement.getGeneratedKeys", ex);
    266               }
    267           }
    268
    269           if (!gotGeneratedKeys) {
    270               insertStatement.close();
    271               insertStatement = null;
    272
    273               idStatement = connection.createStatement();
    274               idStatement.setMaxRows(1);
    275               rs = idStatement.executeQuery(sqlDialect.getSelectInsertId());
    276           }
    277
    278           // A ResultSet cursor is initially positioned before the first row; the
    279           // first call to the method next makes the first row the current row
    280           rs.next();
    281           int eventId = rs.getInt(1);
    282
    283           rs.close();
    284
    285           // we no longer need the insertStatement
    286           if(insertStatement != null) {
    287               insertStatement.close();
    288               insertStatement = null;
    289           }
    290
    291           if(idStatement != null) {
    292               idStatement.close();
    293               idStatement = null;
    294           }
    295
    296           Set propertiesKeys = event.getPropertyKeySet();
    297
    298           if (propertiesKeys.size() > 0) {
    299               PreparedStatement insertPropertiesStatement =
    300                   connection.prepareStatement(insertPropertiesSQL);
    301
    302               for (Iterator i = propertiesKeys.iterator(); i.hasNext();) {
    303                   String key = (String) i.next();
    304                   String value = (String) event.getProperty(key);
    305
    306                   //LogLog.info("id " + eventId + ", key " + key + ", value " + value);
    307                   insertPropertiesStatement.setInt(1, eventId);
    308                   insertPropertiesStatement.setString(2, key);
    309                   insertPropertiesStatement.setString(3, value);
    310
    311                   if (cnxSupportsBatchUpdates) {
    312                       insertPropertiesStatement.addBatch();
    313                   } else {
    314                       insertPropertiesStatement.execute();
    315                   }
    316               }
    317
    318               if (cnxSupportsBatchUpdates) {
    319                   insertPropertiesStatement.executeBatch();
    320               }
    321
    322               insertPropertiesStatement.close();
    323               insertPropertiesStatement = null;
    324           }
    325
    326           String[] strRep = event.getThrowableStrRep();
    327
    328           if (strRep != null) {
    329               LogLog.debug("Logging an exception");
    330
    331               PreparedStatement insertExceptionStatement =
    332                   connection.prepareStatement(insertExceptionSQL);
    333
    334               for (short i = 0; i < strRep.length; i++) {
    335                   insertExceptionStatement.setInt(1, eventId);
    336                   insertExceptionStatement.setShort(2, i);
    337                   insertExceptionStatement.setString(3, strRep[i]);
    338                   if (cnxSupportsBatchUpdates) {
    339                       insertExceptionStatement.addBatch();
    340                   } else {
    341                       insertExceptionStatement.execute();
    342                   }
    343               }
    344               if (cnxSupportsBatchUpdates) {
    345                   insertExceptionStatement.executeBatch();
    346               }
    347               insertExceptionStatement.close();
    348               insertExceptionStatement = null;
    349           }
    350
    351           connection.commit();
    352       } catch (Throwable sqle) {
    353           LogLog.error("problem appending event", sqle);
    354       } finally {
    355           DBHelper.closeConnection(connection);
    356       }
    357   }
    358
    359   public void close() {
    360     closed = true;
    361   }
    362
    363   /**
    364    * Returns value of the <b>LocationInfo </b> property which determines whether
    365    * caller's location info is written to the database.
    366    */
    367   public boolean getLocationInfo() {
    368     return locationInfo;
    369   }
    370
    371   /**
    372    * If true, the information written to the database will include caller's
    373    * location information. Due to performance concerns, by default no location
    374    * information is written to the database.
    375    */
    376   public void setLocationInfo(boolean locationInfo) {
    377     this.locationInfo = locationInfo;
    378   }
    379
    380     /**
    381      * Gets whether appender requires a layout.
    382      * @return false
    383      */
    384   public boolean requiresLayout() {
    385       return false;
    386   }
    387
    388     /**
    389      * {@inheritDoc}
    390      */
    391   public boolean parseUnrecognizedElement(Element element, Properties props) throws Exception {
    392         if ("connectionSource".equals(element.getNodeName())) {
    393             Object instance =
    394                     DOMConfigurator.parseElement(element, props, ConnectionSource.class);
    395             if (instance instanceof ConnectionSource) {
    396                ConnectionSource source = (ConnectionSource) instance;
    397                source.activateOptions();
    398                setConnectionSource(source);
    399             }
    400             return true;
    401         }
    402         return false;
    403   }
    404 }