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 */
017package org.apache.logging.log4j.core.appender.db.jdbc;
018
019import java.io.StringReader;
020import java.sql.Connection;
021import java.sql.PreparedStatement;
022import java.sql.SQLException;
023import java.sql.Timestamp;
024import java.util.ArrayList;
025import java.util.List;
026
027import org.apache.logging.log4j.core.LogEvent;
028import org.apache.logging.log4j.core.appender.AppenderLoggingException;
029import org.apache.logging.log4j.core.appender.ManagerFactory;
030import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager;
031import org.apache.logging.log4j.core.helpers.Closer;
032import org.apache.logging.log4j.core.layout.PatternLayout;
033
034/**
035 * An {@link AbstractDatabaseManager} implementation for relational databases accessed via JDBC.
036 */
037public final class JDBCDatabaseManager extends AbstractDatabaseManager {
038    private static final JDBCDatabaseManagerFactory FACTORY = new JDBCDatabaseManagerFactory();
039
040    private final List<Column> columns;
041    private final ConnectionSource connectionSource;
042    private final String sqlStatement;
043
044    private Connection connection;
045    private PreparedStatement statement;
046
047    private JDBCDatabaseManager(final String name, final int bufferSize, final ConnectionSource connectionSource,
048                                final String sqlStatement, final List<Column> columns) {
049        super(name, bufferSize);
050        this.connectionSource = connectionSource;
051        this.sqlStatement = sqlStatement;
052        this.columns = columns;
053    }
054
055    @Override
056    protected void startupInternal() {
057        // nothing to see here
058    }
059
060    @Override
061    protected void shutdownInternal() {
062        if (this.connection != null || this.statement != null) {
063            this.commitAndClose();
064        }
065    }
066
067    @Override
068    protected void connectAndStart() {
069        try {
070            this.connection = this.connectionSource.getConnection();
071            this.connection.setAutoCommit(false);
072            this.statement = this.connection.prepareStatement(this.sqlStatement);
073        } catch (SQLException e) {
074            throw new AppenderLoggingException(
075                    "Cannot write logging event or flush buffer; JDBC manager cannot connect to the database.", e
076            );
077        }
078    }
079
080    @Override
081    protected void writeInternal(final LogEvent event) {
082        StringReader reader = null;
083        try {
084            if (!this.isRunning() || this.connection == null || this.connection.isClosed() || this.statement == null
085                    || this.statement.isClosed()) {
086                throw new AppenderLoggingException(
087                        "Cannot write logging event; JDBC manager not connected to the database.");
088            }
089
090            int i = 1;
091            for (final Column column : this.columns) {
092                if (column.isEventTimestamp) {
093                    this.statement.setTimestamp(i++, new Timestamp(event.getMillis()));
094                } else {
095                    if (column.isClob) {
096                        reader = new StringReader(column.layout.toSerializable(event));
097                        if (column.isUnicode) {
098                            this.statement.setNClob(i++, reader);
099                        } else {
100                            this.statement.setClob(i++, reader);
101                        }
102                    } else {
103                        if (column.isUnicode) {
104                            this.statement.setNString(i++, column.layout.toSerializable(event));
105                        } else {
106                            this.statement.setString(i++, column.layout.toSerializable(event));
107                        }
108                    }
109                }
110            }
111
112            if (this.statement.executeUpdate() == 0) {
113                throw new AppenderLoggingException(
114                        "No records inserted in database table for log event in JDBC manager.");
115            }
116        } catch (final SQLException e) {
117            throw new AppenderLoggingException("Failed to insert record for log event in JDBC manager: " +
118                    e.getMessage(), e);
119        } finally {
120            Closer.closeSilent(reader);
121        }
122    }
123
124    @Override
125    protected void commitAndClose() {
126        try {
127            if (this.connection != null && !this.connection.isClosed()) {
128                this.connection.commit();
129            }
130        } catch (SQLException e) {
131            throw new AppenderLoggingException("Failed to commit transaction logging event or flushing buffer.", e);
132        } finally {
133            try {
134                if (this.statement != null) {
135                    this.statement.close();
136                }
137            } catch (Exception e) {
138                LOGGER.warn("Failed to close SQL statement logging event or flushing buffer.", e);
139            } finally {
140                this.statement = null;
141            }
142
143            try {
144                if (this.connection != null) {
145                    this.connection.close();
146                }
147            } catch (Exception e) {
148                LOGGER.warn("Failed to close database connection logging event or flushing buffer.", e);
149            } finally {
150                this.connection = null;
151            }
152        }
153    }
154
155    /**
156     * Creates a JDBC manager for use within the {@link JDBCAppender}, or returns a suitable one if it already exists.
157     *
158     * @param name The name of the manager, which should include connection details and hashed passwords where possible.
159     * @param bufferSize The size of the log event buffer.
160     * @param connectionSource The source for connections to the database.
161     * @param tableName The name of the database table to insert log events into.
162     * @param columnConfigs Configuration information about the log table columns.
163     * @return a new or existing JDBC manager as applicable.
164     */
165    public static JDBCDatabaseManager getJDBCDatabaseManager(final String name, final int bufferSize,
166                                                             final ConnectionSource connectionSource,
167                                                             final String tableName,
168                                                             final ColumnConfig[] columnConfigs) {
169
170        return AbstractDatabaseManager.getManager(
171                name, new FactoryData(bufferSize, connectionSource, tableName, columnConfigs), FACTORY
172        );
173    }
174
175    /**
176     * Encapsulates data that {@link JDBCDatabaseManagerFactory} uses to create managers.
177     */
178    private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData {
179        private final ColumnConfig[] columnConfigs;
180        private final ConnectionSource connectionSource;
181        private final String tableName;
182
183        protected FactoryData(final int bufferSize, final ConnectionSource connectionSource, final String tableName,
184                              final ColumnConfig[] columnConfigs) {
185            super(bufferSize);
186            this.connectionSource = connectionSource;
187            this.tableName = tableName;
188            this.columnConfigs = columnConfigs;
189        }
190    }
191
192    /**
193     * Creates managers.
194     */
195    private static final class JDBCDatabaseManagerFactory implements ManagerFactory<JDBCDatabaseManager, FactoryData> {
196        @Override
197        public JDBCDatabaseManager createManager(final String name, final FactoryData data) {
198            final StringBuilder columnPart = new StringBuilder();
199            final StringBuilder valuePart = new StringBuilder();
200            final List<Column> columns = new ArrayList<Column>();
201            int i = 0;
202            for (final ColumnConfig config : data.columnConfigs) {
203                if (i++ > 0) {
204                    columnPart.append(',');
205                    valuePart.append(',');
206                }
207
208                columnPart.append(config.getColumnName());
209
210                if (config.getLiteralValue() != null) {
211                    valuePart.append(config.getLiteralValue());
212                } else {
213                    columns.add(new Column(
214                            config.getLayout(), config.isEventTimestamp(), config.isUnicode(), config.isClob()
215                    ));
216                    valuePart.append('?');
217                }
218            }
219
220            final String sqlStatement = "INSERT INTO " + data.tableName + " (" + columnPart + ") VALUES (" +
221                    valuePart + ")";
222
223            return new JDBCDatabaseManager(name, data.getBufferSize(), data.connectionSource, sqlStatement, columns);
224        }
225    }
226
227    /**
228     * Encapsulates information about a database column and how to persist data to it.
229     */
230    private static final class Column {
231        private final PatternLayout layout;
232        private final boolean isEventTimestamp;
233        private final boolean isUnicode;
234        private final boolean isClob;
235
236        private Column(final PatternLayout layout, final boolean isEventDate, final boolean isUnicode,
237                       final boolean isClob) {
238            this.layout = layout;
239            this.isEventTimestamp = isEventDate;
240            this.isUnicode = isUnicode;
241            this.isClob = isClob;
242        }
243    }
244}