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