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.logging.log4j.core.appender.db.jdbc;
    18
    19  import java.io.StringReader;
    20  import java.sql.Connection;
    21  import java.sql.DatabaseMetaData;
    22  import java.sql.PreparedStatement;
    23  import java.sql.SQLException;
    24  import java.sql.Timestamp;
    25  import java.util.ArrayList;
    26  import java.util.List;
    27
    28  import org.apache.logging.log4j.core.LogEvent;
    29  import org.apache.logging.log4j.core.appender.AppenderLoggingException;
    30  import org.apache.logging.log4j.core.appender.ManagerFactory;
    31  import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager;
    32  import org.apache.logging.log4j.core.layout.PatternLayout;
    33  import org.apache.logging.log4j.core.util.Closer;
    34
    35  /**
    36   * An {@link AbstractDatabaseManager} implementation for relational databases accessed via JDBC.
    37   */
    38  public final class JdbcDatabaseManager extends AbstractDatabaseManager {
    39
    40      private static final JdbcDatabaseManagerFactory INSTANCE = new JdbcDatabaseManagerFactory();
    41
    42      private final List<Column> columns;
    43      private final ConnectionSource connectionSource;
    44      private final String sqlStatement;
    45
    46      private Connection connection;
    47      private PreparedStatement statement;
    48      private boolean isBatchSupported;
    49
    50      private JdbcDatabaseManager(final String name, final int bufferSize, final ConnectionSource connectionSource,
    51                                  final String sqlStatement, final List<Column> columns) {
    52          super(name, bufferSize);
    53          this.connectionSource = connectionSource;
    54          this.sqlStatement = sqlStatement;
    55          this.columns = columns;
    56      }
    57
    58      @Override
    59      protected void startupInternal() throws Exception {
    60          this.connection = this.connectionSource.getConnection();
    61          final DatabaseMetaData metaData = this.connection.getMetaData();
    62          this.isBatchSupported = metaData.supportsBatchUpdates();
    63          Closer.closeSilently(this.connection);
    64      }
    65
    66      @Override
    67      protected void shutdownInternal() {
    68          if (this.connection != null || this.statement != null) {
    69              this.commitAndClose();
    70          }
    71      }
    72
    73      @Override
    74      protected void connectAndStart() {
    75          try {
    76              this.connection = this.connectionSource.getConnection();
    77              this.connection.setAutoCommit(false);
    78              this.statement = this.connection.prepareStatement(this.sqlStatement);
    79          } catch (final SQLException e) {
    80              throw new AppenderLoggingException(
    81                      "Cannot write logging event or flush buffer; JDBC manager cannot connect to the database.", e
    82              );
    83          }
    84      }
    85
    86      @Override
    87      protected void writeInternal(final LogEvent event) {
    88          StringReader reader = null;
    89          try {
    90              if (!this.isRunning() || this.connection == null || this.connection.isClosed() || this.statement == null
    91                      || this.statement.isClosed()) {
    92                  throw new AppenderLoggingException(
    93                          "Cannot write logging event; JDBC manager not connected to the database.");
    94              }
    95
    96              int i = 1;
    97              for (final Column column : this.columns) {
    98                  if (column.isEventTimestamp) {
    99                      this.statement.setTimestamp(i++, new Timestamp(event.getTimeMillis()));
    100                 } else {
    101                     if (column.isClob) {
    102                         reader = new StringReader(column.layout.toSerializable(event));
    103                         if (column.isUnicode) {
    104                             this.statement.setNClob(i++, reader);
    105                         } else {
    106                             this.statement.setClob(i++, reader);
    107                         }
    108                     } else {
    109                         if (column.isUnicode) {
    110                             this.statement.setNString(i++, column.layout.toSerializable(event));
    111                         } else {
    112                             this.statement.setString(i++, column.layout.toSerializable(event));
    113                         }
    114                     }
    115                 }
    116             }
    117
    118             if (this.isBatchSupported) {
    119                 this.statement.addBatch();
    120             } else if (this.statement.executeUpdate() == 0) {
    121                 throw new AppenderLoggingException(
    122                         "No records inserted in database table for log event in JDBC manager.");
    123             }
    124         } catch (final SQLException e) {
    125             throw new AppenderLoggingException("Failed to insert record for log event in JDBC manager: " +
    126                     e.getMessage(), e);
    127         } finally {
    128             Closer.closeSilently(reader);
    129         }
    130     }
    131
    132     @Override
    133     protected void commitAndClose() {
    134         try {
    135             if (this.connection != null && !this.connection.isClosed()) {
    136                 if (this.isBatchSupported) {
    137                     this.statement.executeBatch();
    138                 }
    139                 this.connection.commit();
    140             }
    141         } catch (final SQLException e) {
    142             throw new AppenderLoggingException("Failed to commit transaction logging event or flushing buffer.", e);
    143         } finally {
    144             try {
    145                 Closer.close(this.statement);
    146             } catch (final Exception e) {
    147                 LOGGER.warn("Failed to close SQL statement logging event or flushing buffer.", e);
    148             } finally {
    149                 this.statement = null;
    150             }
    151
    152             try {
    153                 Closer.close(this.connection);
    154             } catch (final Exception e) {
    155                 LOGGER.warn("Failed to close database connection logging event or flushing buffer.", e);
    156             } finally {
    157                 this.connection = null;
    158             }
    159         }
    160     }
    161
    162     /**
    163      * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
    164      *
    165      * @param name The name of the manager, which should include connection details and hashed passwords where possible.
    166      * @param bufferSize The size of the log event buffer.
    167      * @param connectionSource The source for connections to the database.
    168      * @param tableName The name of the database table to insert log events into.
    169      * @param columnConfigs Configuration information about the log table columns.
    170      * @return a new or existing JDBC manager as applicable.
    171      */
    172     public static JdbcDatabaseManager getJDBCDatabaseManager(final String name, final int bufferSize,
    173                                                              final ConnectionSource connectionSource,
    174                                                              final String tableName,
    175                                                              final ColumnConfig[] columnConfigs) {
    176
    177         return AbstractDatabaseManager.getManager(
    178                 name, new FactoryData(bufferSize, connectionSource, tableName, columnConfigs), getFactory()
    179         );
    180     }
    181
    182     private static JdbcDatabaseManagerFactory getFactory() {
    183         return INSTANCE;
    184     }
    185
    186     /**
    187      * Encapsulates data that {@link JdbcDatabaseManagerFactory} uses to create managers.
    188      */
    189     private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData {
    190         private final ColumnConfig[] columnConfigs;
    191         private final ConnectionSource connectionSource;
    192         private final String tableName;
    193
    194         protected FactoryData(final int bufferSize, final ConnectionSource connectionSource, final String tableName,
    195                               final ColumnConfig[] columnConfigs) {
    196             super(bufferSize);
    197             this.connectionSource = connectionSource;
    198             this.tableName = tableName;
    199             this.columnConfigs = columnConfigs;
    200         }
    201     }
    202
    203     /**
    204      * Creates managers.
    205      */
    206     private static final class JdbcDatabaseManagerFactory implements ManagerFactory<JdbcDatabaseManager, FactoryData> {
    207         @Override
    208         public JdbcDatabaseManager createManager(final String name, final FactoryData data) {
    209             final StringBuilder columnPart = new StringBuilder();
    210             final StringBuilder valuePart = new StringBuilder();
    211             final List<Column> columns = new ArrayList<Column>();
    212             int i = 0;
    213             for (final ColumnConfig config : data.columnConfigs) {
    214                 if (i++ > 0) {
    215                     columnPart.append(',');
    216                     valuePart.append(',');
    217                 }
    218
    219                 columnPart.append(config.getColumnName());
    220
    221                 if (config.getLiteralValue() != null) {
    222                     valuePart.append(config.getLiteralValue());
    223                 } else {
    224                     columns.add(new Column(
    225                             config.getLayout(), config.isEventTimestamp(), config.isUnicode(), config.isClob()
    226                     ));
    227                     valuePart.append('?');
    228                 }
    229             }
    230
    231             final String sqlStatement = "INSERT INTO " + data.tableName + " (" + columnPart + ") VALUES (" +
    232                     valuePart + ')';
    233
    234             return new JdbcDatabaseManager(name, data.getBufferSize(), data.connectionSource, sqlStatement, columns);
    235         }
    236     }
    237
    238     /**
    239      * Encapsulates information about a database column and how to persist data to it.
    240      */
    241     private static final class Column {
    242         private final PatternLayout layout;
    243         private final boolean isEventTimestamp;
    244         private final boolean isUnicode;
    245         private final boolean isClob;
    246
    247         private Column(final PatternLayout layout, final boolean isEventDate, final boolean isUnicode,
    248                        final boolean isClob) {
    249             this.layout = layout;
    250             this.isEventTimestamp = isEventDate;
    251             this.isUnicode = isUnicode;
    252             this.isClob = isClob;
    253         }
    254     }
    255 }