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.Serializable;
020import java.io.StringReader;
021import java.sql.Clob;
022import java.sql.Connection;
023import java.sql.DatabaseMetaData;
024import java.sql.NClob;
025import java.sql.PreparedStatement;
026import java.sql.SQLException;
027import java.sql.Timestamp;
028import java.sql.Types;
029import java.util.ArrayList;
030import java.util.Date;
031import java.util.List;
032import java.util.Objects;
033
034import org.apache.logging.log4j.core.Layout;
035import org.apache.logging.log4j.core.LogEvent;
036import org.apache.logging.log4j.core.StringLayout;
037import org.apache.logging.log4j.core.appender.AppenderLoggingException;
038import org.apache.logging.log4j.core.appender.ManagerFactory;
039import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager;
040import org.apache.logging.log4j.core.appender.db.ColumnMapping;
041import org.apache.logging.log4j.core.config.plugins.convert.DateTypeConverter;
042import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
043import org.apache.logging.log4j.core.util.Closer;
044import org.apache.logging.log4j.message.MapMessage;
045import org.apache.logging.log4j.spi.ThreadContextMap;
046import org.apache.logging.log4j.spi.ThreadContextStack;
047import org.apache.logging.log4j.status.StatusLogger;
048import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
049import org.apache.logging.log4j.util.ReadOnlyStringMap;
050import org.apache.logging.log4j.util.Strings;
051
052/**
053 * An {@link AbstractDatabaseManager} implementation for relational databases accessed via JDBC.
054 */
055public final class JdbcDatabaseManager extends AbstractDatabaseManager {
056
057    private static StatusLogger logger() {
058        return StatusLogger.getLogger();
059    }
060
061    private static final JdbcDatabaseManagerFactory INSTANCE = new JdbcDatabaseManagerFactory();
062
063    // NOTE: prepared statements are prepared in this order: column mappings, then column configs
064    private final List<ColumnMapping> columnMappings;
065    private final List<ColumnConfig> columnConfigs;
066    private final ConnectionSource connectionSource;
067    private final String sqlStatement;
068
069    private Connection connection;
070    private PreparedStatement statement;
071    private boolean isBatchSupported;
072
073    private JdbcDatabaseManager(final String name, final int bufferSize, final ConnectionSource connectionSource,
074                                final String sqlStatement, final List<ColumnConfig> columnConfigs,
075                                final List<ColumnMapping> columnMappings) {
076        super(name, bufferSize);
077        this.connectionSource = connectionSource;
078        this.sqlStatement = sqlStatement;
079        this.columnConfigs = columnConfigs;
080        this.columnMappings = columnMappings;
081    }
082
083    @Override
084    protected void startupInternal() throws Exception {
085        this.connection = this.connectionSource.getConnection();
086        final DatabaseMetaData metaData = this.connection.getMetaData();
087        this.isBatchSupported = metaData.supportsBatchUpdates();
088        logger().debug("Closing Connection {}", this.connection);
089        Closer.closeSilently(this.connection);
090    }
091
092    @Override
093    protected boolean shutdownInternal() {
094        if (this.connection != null || this.statement != null) {
095            return this.commitAndClose();
096        }
097        if (connectionSource != null) {
098            connectionSource.stop();
099        }
100        return true;
101    }
102
103    @Override
104    protected void connectAndStart() {
105        try {
106            this.connection = this.connectionSource.getConnection();
107            this.connection.setAutoCommit(false);
108            logger().debug("Preparing SQL: {}", this.sqlStatement);
109            this.statement = this.connection.prepareStatement(this.sqlStatement);
110        } catch (final SQLException e) {
111            throw new AppenderLoggingException(
112                    "Cannot write logging event or flush buffer; JDBC manager cannot connect to the database.", e);
113        }
114    }
115
116    @Deprecated
117    @Override
118    protected void writeInternal(final LogEvent event) {
119        writeInternal(event, null);
120    }
121    
122    private void setFields(final MapMessage<?, ?> mapMessage) throws SQLException {
123        final IndexedReadOnlyStringMap map = mapMessage.getIndexedReadOnlyStringMap();
124        final String simpleName = statement.getClass().getName();
125        int i = 1; // JDBC indices start at 1
126        for (final ColumnMapping mapping : this.columnMappings) {
127            final String source = mapping.getSource();
128            final String key = Strings.isEmpty(source) ? mapping.getName() : source;
129            final Object value = map.getValue(key);
130            if (logger().isTraceEnabled()) {
131                final String valueStr = value instanceof String ? "\"" + value + "\"" : Objects.toString(value, null);
132                logger().trace("{} setObject({}, {}) for key '{}' and mapping '{}'", simpleName, i, valueStr, key,
133                        mapping.getName());
134            }
135            statement.setObject(i++, value);
136        }
137    }
138
139    @Override
140    protected void writeInternal(final LogEvent event, final Serializable serializable) {
141        StringReader reader = null;
142        try {
143            if (!this.isRunning() || this.connection == null || this.connection.isClosed() || this.statement == null
144                    || this.statement.isClosed()) {
145                throw new AppenderLoggingException(
146                        "Cannot write logging event; JDBC manager not connected to the database.");
147            }
148
149            if (serializable instanceof MapMessage) {
150                setFields((MapMessage<?, ?>) serializable);
151            }
152            int i = 1; // JDBC indices start at 1
153            for (final ColumnMapping mapping : this.columnMappings) {
154                if (ThreadContextMap.class.isAssignableFrom(mapping.getType())
155                        || ReadOnlyStringMap.class.isAssignableFrom(mapping.getType())) {
156                    this.statement.setObject(i++, event.getContextData().toMap());
157                } else if (ThreadContextStack.class.isAssignableFrom(mapping.getType())) {
158                    this.statement.setObject(i++, event.getContextStack().asList());
159                } else if (Date.class.isAssignableFrom(mapping.getType())) {
160                    this.statement.setObject(i++, DateTypeConverter.fromMillis(event.getTimeMillis(),
161                            mapping.getType().asSubclass(Date.class)));
162                } else {
163                    StringLayout layout = mapping.getLayout();
164                    if (layout != null) {
165                        if (Clob.class.isAssignableFrom(mapping.getType())) {
166                            this.statement.setClob(i++, new StringReader(layout.toSerializable(event)));
167                        } else if (NClob.class.isAssignableFrom(mapping.getType())) {
168                            this.statement.setNClob(i++, new StringReader(layout.toSerializable(event)));
169                        } else {
170                            final Object value = TypeConverters.convert(layout.toSerializable(event), mapping.getType(),
171                                    null);
172                            if (value == null) {
173                                this.statement.setNull(i++, Types.NULL);
174                            } else {
175                                this.statement.setObject(i++, value);
176                            }
177                        }
178                    }
179                }
180            }
181            for (final ColumnConfig column : this.columnConfigs) {
182                if (column.isEventTimestamp()) {
183                    this.statement.setTimestamp(i++, new Timestamp(event.getTimeMillis()));
184                } else if (column.isClob()) {
185                    reader = new StringReader(column.getLayout().toSerializable(event));
186                    if (column.isUnicode()) {
187                        this.statement.setNClob(i++, reader);
188                    } else {
189                        this.statement.setClob(i++, reader);
190                    }
191                } else if (column.isUnicode()) {
192                    this.statement.setNString(i++, column.getLayout().toSerializable(event));
193                } else {
194                    this.statement.setString(i++, column.getLayout().toSerializable(event));
195                }
196            }
197
198            if (this.isBatchSupported) {
199                this.statement.addBatch();
200            } else if (this.statement.executeUpdate() == 0) {
201                throw new AppenderLoggingException(
202                        "No records inserted in database table for log event in JDBC manager.");
203            }
204        } catch (final SQLException e) {
205            throw new AppenderLoggingException("Failed to insert record for log event in JDBC manager: " +
206                    e.getMessage(), e);
207        } finally {
208            Closer.closeSilently(reader);
209        }
210    }
211
212    @Override
213    protected boolean commitAndClose() {
214        boolean closed = true;
215        try {
216            if (this.connection != null && !this.connection.isClosed()) {
217                if (this.isBatchSupported) {
218                    logger().debug("Executing batch PreparedStatement {}", this.statement);
219                    this.statement.executeBatch();
220                }
221                logger().debug("Committing Connection {}", this.connection);
222                this.connection.commit();
223            }
224        } catch (final SQLException e) {
225            throw new AppenderLoggingException("Failed to commit transaction logging event or flushing buffer.", e);
226        } finally {
227            try {
228                logger().debug("Closing PreparedStatement {}", this.statement);
229                Closer.close(this.statement);
230            } catch (final Exception e) {
231                logWarn("Failed to close SQL statement logging event or flushing buffer", e);
232                closed = false;
233            } finally {
234                this.statement = null;
235            }
236
237            try {
238                logger().debug("Closing Connection {}", this.connection);
239                Closer.close(this.connection);
240            } catch (final Exception e) {
241                logWarn("Failed to close database connection logging event or flushing buffer", e);
242                closed = false;
243            } finally {
244                this.connection = null;
245            }
246        }
247        return closed;
248    }
249
250    /**
251     * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
252     *
253     * @param name The name of the manager, which should include connection details and hashed passwords where possible.
254     * @param bufferSize The size of the log event buffer.
255     * @param connectionSource The source for connections to the database.
256     * @param tableName The name of the database table to insert log events into.
257     * @param columnConfigs Configuration information about the log table columns.
258     * @return a new or existing JDBC manager as applicable.
259     * @deprecated use {@link #getManager(String, int, Layout, ConnectionSource, String, ColumnConfig[], ColumnMapping[])}
260     */
261    @Deprecated
262    public static JdbcDatabaseManager getJDBCDatabaseManager(final String name, final int bufferSize,
263                                                             final ConnectionSource connectionSource,
264                                                             final String tableName,
265                                                             final ColumnConfig[] columnConfigs) {
266
267        return getManager(name,
268            new FactoryData(bufferSize, null, connectionSource, tableName, columnConfigs, new ColumnMapping[0]),
269            getFactory());
270    }
271
272    /**
273     * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
274     *
275     * @param name The name of the manager, which should include connection details and hashed passwords where possible.
276     * @param bufferSize The size of the log event buffer.
277     * @param connectionSource The source for connections to the database.
278     * @param tableName The name of the database table to insert log events into.
279     * @param columnConfigs Configuration information about the log table columns.
280     * @param columnMappings column mapping configuration (including type conversion).
281     * @return a new or existing JDBC manager as applicable.
282     * @deprecated use {@link #getManager(String, int, Layout, ConnectionSource, String, ColumnConfig[], ColumnMapping[])}
283     */
284    @Deprecated
285    public static JdbcDatabaseManager getManager(final String name,
286                                                 final int bufferSize,
287                                                 final ConnectionSource connectionSource,
288                                                 final String tableName,
289                                                 final ColumnConfig[] columnConfigs,
290                                                 final ColumnMapping[] columnMappings) {
291        return getManager(name, new FactoryData(bufferSize, null, connectionSource, tableName, columnConfigs, columnMappings),
292            getFactory());
293    }
294
295    /**
296     * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
297     *
298     * @param name The name of the manager, which should include connection details and hashed passwords where possible.
299     * @param bufferSize The size of the log event buffer.
300     * @param layout The Appender-level layout
301     * @param connectionSource The source for connections to the database.
302     * @param tableName The name of the database table to insert log events into.
303     * @param columnConfigs Configuration information about the log table columns.
304     * @param columnMappings column mapping configuration (including type conversion).
305     * @return a new or existing JDBC manager as applicable.
306     */
307    public static JdbcDatabaseManager getManager(final String name,
308                                                 final int bufferSize,
309                                                 final Layout<? extends Serializable> layout,
310                                                 final ConnectionSource connectionSource,
311                                                 final String tableName,
312                                                 final ColumnConfig[] columnConfigs,
313                                                 final ColumnMapping[] columnMappings) {
314        return getManager(name, new FactoryData(bufferSize, layout, connectionSource, tableName, columnConfigs, columnMappings),
315            getFactory());
316    }
317
318    private static JdbcDatabaseManagerFactory getFactory() {
319        return INSTANCE;
320    }
321
322    /**
323     * Encapsulates data that {@link JdbcDatabaseManagerFactory} uses to create managers.
324     */
325    private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData {
326        private final ConnectionSource connectionSource;
327        private final String tableName;
328        private final ColumnConfig[] columnConfigs;
329        private final ColumnMapping[] columnMappings;
330
331        protected FactoryData(final int bufferSize, final Layout<? extends Serializable> layout,
332                final ConnectionSource connectionSource, final String tableName, final ColumnConfig[] columnConfigs,
333                final ColumnMapping[] columnMappings) {
334            super(bufferSize, layout);
335            this.connectionSource = connectionSource;
336            this.tableName = tableName;
337            this.columnConfigs = columnConfigs;
338            this.columnMappings = columnMappings;
339        }
340    }
341
342    /**
343     * Creates managers.
344     */
345    private static final class JdbcDatabaseManagerFactory implements ManagerFactory<JdbcDatabaseManager, FactoryData> {
346        
347        private static final char PARAMETER_MARKER = '?';
348
349        @Override
350        public JdbcDatabaseManager createManager(final String name, final FactoryData data) {
351            final StringBuilder sb = new StringBuilder("INSERT INTO ").append(data.tableName).append(" (");
352            // so this gets a little more complicated now that there are two ways to configure column mappings, but
353            // both mappings follow the same exact pattern for the prepared statement
354            int i = 1;
355            for (final ColumnMapping mapping : data.columnMappings) {
356                final  String mappingName = mapping.getName();
357                logger().trace("Adding INSERT ColumnMapping[{}]: {}={} ", i++, mappingName, mapping);
358                sb.append(mappingName).append(',');
359            }
360            for (final ColumnConfig config : data.columnConfigs) {
361                sb.append(config.getColumnName()).append(',');
362            }
363            // at least one of those arrays is guaranteed to be non-empty
364            sb.setCharAt(sb.length() - 1, ')');
365            sb.append(" VALUES (");
366            i = 1;
367            final List<ColumnMapping> columnMappings = new ArrayList<>(data.columnMappings.length);
368            for (final ColumnMapping mapping : data.columnMappings) {
369                final String mappingName = mapping.getName();
370                if (Strings.isNotEmpty(mapping.getLiteralValue())) {
371                    logger().trace("Adding INSERT VALUES literal for ColumnMapping[{}]: {}={} ", i, mappingName, mapping.getLiteralValue());
372                    sb.append(mapping.getLiteralValue());
373                }
374                if (Strings.isNotEmpty(mapping.getParameter())) {
375                    logger().trace("Adding INSERT VALUES parameter for ColumnMapping[{}]: {}={} ", i, mappingName, mapping.getParameter());
376                    sb.append(mapping.getParameter());
377                    columnMappings.add(mapping);
378                } else {
379                    logger().trace("Adding INSERT VALUES parameter marker for ColumnMapping[{}]: {}={} ", i, mappingName, PARAMETER_MARKER);
380                    sb.append(PARAMETER_MARKER);
381                    columnMappings.add(mapping);
382                }
383                sb.append(',');
384                i++;
385            }
386            final List<ColumnConfig> columnConfigs = new ArrayList<>(data.columnConfigs.length);
387            for (final ColumnConfig config : data.columnConfigs) {
388                if (Strings.isNotEmpty(config.getLiteralValue())) {
389                    sb.append(config.getLiteralValue());
390                } else {
391                    sb.append(PARAMETER_MARKER);
392                    columnConfigs.add(config);
393                }
394                sb.append(',');
395            }
396            // at least one of those arrays is guaranteed to be non-empty
397            sb.setCharAt(sb.length() - 1, ')');
398            final String sqlStatement = sb.toString();
399
400            return new JdbcDatabaseManager(name, data.getBufferSize(), data.connectionSource, sqlStatement,
401                columnConfigs, columnMappings);
402        }
403    }
404
405}