    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements. See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache license, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License. You may obtain a copy of the License at
    008     *
    009     *      http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the license for the specific language governing permissions and
    015     * limitations under the license.
    016     */
    017    package org.apache.logging.log4j.core.appender.db.jdbc;
    018
    019    import java.io.StringReader;
    020    import java.sql.Connection;
    021    import java.sql.DatabaseMetaData;
    022    import java.sql.PreparedStatement;
    023    import java.sql.SQLException;
    024    import java.sql.Timestamp;
    025    import java.util.ArrayList;
    026    import java.util.List;
    027
    028    import org.apache.logging.log4j.core.LogEvent;
    029    import org.apache.logging.log4j.core.appender.AppenderLoggingException;
    030    import org.apache.logging.log4j.core.appender.ManagerFactory;
    031    import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager;
    032    import org.apache.logging.log4j.core.layout.PatternLayout;
    033    import org.apache.logging.log4j.core.util.Closer;
    034
    035    /**
    036     * An {@link AbstractDatabaseManager} implementation for relational databases accessed via JDBC.
    037     */
    038    public final class JdbcDatabaseManager extends AbstractDatabaseManager {
    039
    040        private static final JdbcDatabaseManagerFactory INSTANCE = new JdbcDatabaseManagerFactory();
    041
    042        private final List<Column> columns;
    043        private final ConnectionSource connectionSource;
    044        private final String sqlStatement;
    045
    046        private Connection connection;
    047        private PreparedStatement statement;
    048        private boolean isBatchSupported;
    049
    050        private JdbcDatabaseManager(final String name, final int bufferSize, final ConnectionSource connectionSource,
    051                                    final String sqlStatement, final List<Column> columns) {
    052            super(name, bufferSize);
    053            this.connectionSource = connectionSource;
    054            this.sqlStatement = sqlStatement;
    055            this.columns = columns;
    056        }
    057
    058        @Override
    059        protected void startupInternal() throws Exception {
    060            this.connection = this.connectionSource.getConnection();
    061            final DatabaseMetaData metaData = this.connection.getMetaData();
    062            this.isBatchSupported = metaData.supportsBatchUpdates();
    063            Closer.closeSilently(this.connection);
    064        }
    065
    066        @Override
    067        protected void shutdownInternal() {
    068            if (this.connection != null || this.statement != null) {
    069                this.commitAndClose();
    070            }
    071        }
    072
    073        @Override
    074        protected void connectAndStart() {
    075            try {
    076                this.connection = this.connectionSource.getConnection();
    077                this.connection.setAutoCommit(false);
    078                this.statement = this.connection.prepareStatement(this.sqlStatement);
    079            } catch (final SQLException e) {
    080                throw new AppenderLoggingException(
    081                        "Cannot write logging event or flush buffer; JDBC manager cannot connect to the database.", e
    082                );
    083            }
    084        }
    085
    086        @Override
    087        protected void writeInternal(final LogEvent event) {
    088            StringReader reader = null;
    089            try {
    090                if (!this.isRunning() || this.connection == null || this.connection.isClosed() || this.statement == null
    091                        || this.statement.isClosed()) {
    092                    throw new AppenderLoggingException(
    093                            "Cannot write logging event; JDBC manager not connected to the database.");
    094                }
    095
    096                int i = 1;
    097                for (final Column column : this.columns) {
    098                    if (column.isEventTimestamp) {
    099                        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    }