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.Serializable;
20  import java.io.StringReader;
21  import java.sql.Clob;
22  import java.sql.Connection;
23  import java.sql.DatabaseMetaData;
24  import java.sql.NClob;
25  import java.sql.PreparedStatement;
26  import java.sql.ResultSetMetaData;
27  import java.sql.SQLException;
28  import java.sql.SQLTransactionRollbackException;
29  import java.sql.Statement;
30  import java.sql.Timestamp;
31  import java.sql.Types;
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.Date;
35  import java.util.HashMap;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Objects;
39  import java.util.concurrent.CountDownLatch;
40  
41  import org.apache.logging.log4j.core.Layout;
42  import org.apache.logging.log4j.core.LogEvent;
43  import org.apache.logging.log4j.core.StringLayout;
44  import org.apache.logging.log4j.core.appender.AppenderLoggingException;
45  import org.apache.logging.log4j.core.appender.ManagerFactory;
46  import org.apache.logging.log4j.core.appender.db.AbstractDatabaseAppender;
47  import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager;
48  import org.apache.logging.log4j.core.appender.db.ColumnMapping;
49  import org.apache.logging.log4j.core.appender.db.DbAppenderLoggingException;
50  import org.apache.logging.log4j.core.config.plugins.convert.DateTypeConverter;
51  import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
52  import org.apache.logging.log4j.core.util.Closer;
53  import org.apache.logging.log4j.core.util.Log4jThread;
54  import org.apache.logging.log4j.message.MapMessage;
55  import org.apache.logging.log4j.spi.ThreadContextMap;
56  import org.apache.logging.log4j.spi.ThreadContextStack;
57  import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
58  import org.apache.logging.log4j.util.ReadOnlyStringMap;
59  import org.apache.logging.log4j.util.Strings;
60  
61  /**
62   * An {@link AbstractDatabaseManager} implementation for relational databases accessed via JDBC.
63   */
64  public final class JdbcDatabaseManager extends AbstractDatabaseManager {
65  
66      /**
67       * Encapsulates data that {@link JdbcDatabaseManagerFactory} uses to create managers.
68       */
69      private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData {
70          private final ConnectionSource connectionSource;
71          private final String tableName;
72          private final ColumnConfig[] columnConfigs;
73          private final ColumnMapping[] columnMappings;
74          private final boolean immediateFail;
75          private final boolean retry;
76          private final long reconnectIntervalMillis;
77          private final boolean truncateStrings;
78  
79          protected FactoryData(final int bufferSize, final Layout<? extends Serializable> layout,
80                  final ConnectionSource connectionSource, final String tableName, final ColumnConfig[] columnConfigs,
81                  final ColumnMapping[] columnMappings, final boolean immediateFail, final long reconnectIntervalMillis,
82                  final boolean truncateStrings) {
83              super(bufferSize, layout);
84              this.connectionSource = connectionSource;
85              this.tableName = tableName;
86              this.columnConfigs = columnConfigs;
87              this.columnMappings = columnMappings;
88              this.immediateFail = immediateFail;
89              this.retry = reconnectIntervalMillis > 0;
90              this.reconnectIntervalMillis = reconnectIntervalMillis;
91              this.truncateStrings = truncateStrings;
92          }
93  
94          @Override
95          public String toString() {
96              return String.format(
97                      "FactoryData [connectionSource=%s, tableName=%s, columnConfigs=%s, columnMappings=%s, immediateFail=%s, retry=%s, reconnectIntervalMillis=%s, truncateStrings=%s]",
98                      connectionSource, tableName, Arrays.toString(columnConfigs), Arrays.toString(columnMappings),
99                      immediateFail, retry, reconnectIntervalMillis, truncateStrings);
100         }
101     }
102 
103     /**
104      * Creates managers.
105      */
106     private static final class JdbcDatabaseManagerFactory implements ManagerFactory<JdbcDatabaseManager, FactoryData> {
107 
108         private static final char PARAMETER_MARKER = '?';
109 
110         @Override
111         public JdbcDatabaseManager createManager(final String name, final FactoryData data) {
112             final StringBuilder sb = new StringBuilder("insert into ").append(data.tableName).append(" (");
113             // so this gets a little more complicated now that there are two ways to configure column mappings, but
114             // both mappings follow the same exact pattern for the prepared statement
115             appendColumnNames("INSERT", data, sb);
116             sb.append(") values (");
117             int i = 1;
118             if (data.columnMappings != null) {
119                 for (final ColumnMapping mapping : data.columnMappings) {
120                     final String mappingName = mapping.getName();
121                     if (Strings.isNotEmpty(mapping.getLiteralValue())) {
122                         logger().trace("Adding INSERT VALUES literal for ColumnMapping[{}]: {}={} ", i, mappingName,
123                                 mapping.getLiteralValue());
124                         sb.append(mapping.getLiteralValue());
125                     } else if (Strings.isNotEmpty(mapping.getParameter())) {
126                         logger().trace("Adding INSERT VALUES parameter for ColumnMapping[{}]: {}={} ", i, mappingName,
127                                 mapping.getParameter());
128                         sb.append(mapping.getParameter());
129                     } else {
130                         logger().trace("Adding INSERT VALUES parameter marker for ColumnMapping[{}]: {}={} ", i,
131                                 mappingName, PARAMETER_MARKER);
132                         sb.append(PARAMETER_MARKER);
133                     }
134                     sb.append(',');
135                     i++;
136                 }
137             }
138             final int columnConfigsLen = data.columnConfigs == null ? 0 : data.columnConfigs.length;
139             final List<ColumnConfig> columnConfigs = new ArrayList<>(columnConfigsLen);
140             if (data.columnConfigs != null) {
141                 for (final ColumnConfig config : data.columnConfigs) {
142                     if (Strings.isNotEmpty(config.getLiteralValue())) {
143                         sb.append(config.getLiteralValue());
144                     } else {
145                         sb.append(PARAMETER_MARKER);
146                         columnConfigs.add(config);
147                     }
148                     sb.append(',');
149                 }
150             }
151             // at least one of those arrays is guaranteed to be non-empty
152             sb.setCharAt(sb.length() - 1, ')');
153             final String sqlStatement = sb.toString();
154 
155             return new JdbcDatabaseManager(name, sqlStatement, columnConfigs, data);
156         }
157     }
158 
159     /**
160      * Handles reconnecting to JDBC once on a Thread.
161      */
162     private final class Reconnector extends Log4jThread {
163 
164         private final CountDownLatch latch = new CountDownLatch(1);
165         private volatile boolean shutdown = false;
166 
167         private Reconnector() {
168             super("JdbcDatabaseManager-Reconnector");
169         }
170 
171         public void latch() {
172             try {
173                 latch.await();
174             } catch (final InterruptedException ex) {
175                 // Ignore the exception.
176             }
177         }
178 
179         void reconnect() throws SQLException {
180             closeResources(false);
181             connectAndPrepare();
182             reconnector = null;
183             shutdown = true;
184             logger().debug("Connection reestablished to {}", factoryData);
185         }
186 
187         @Override
188         public void run() {
189             while (!shutdown) {
190                 try {
191                     sleep(factoryData.reconnectIntervalMillis);
192                     reconnect();
193                 } catch (final InterruptedException | SQLException e) {
194                     logger().debug("Cannot reestablish JDBC connection to {}: {}", factoryData, e.getLocalizedMessage(),
195                             e);
196                 } finally {
197                     latch.countDown();
198                 }
199             }
200         }
201 
202         public void shutdown() {
203             shutdown = true;
204         }
205 
206         @Override
207         public String toString() {
208             return String.format("Reconnector [latch=%s, shutdown=%s]", latch, shutdown);
209         }
210 
211     }
212 
213     private static final class ResultSetColumnMetaData {
214 
215         private final String schemaName;
216         private final String catalogName;
217         private final String tableName;
218         private final String name;
219         private final String nameKey;
220         private final String label;
221         private final int displaySize;
222         private final int type;
223         private final String typeName;
224         private final String className;
225         private final int precision;
226         private final int scale;
227         private final boolean isStringType;
228 
229         public ResultSetColumnMetaData(final ResultSetMetaData rsMetaData, final int j) throws SQLException {
230             // @formatter:off
231             this(rsMetaData.getSchemaName(j),
232                  rsMetaData.getCatalogName(j),
233                  rsMetaData.getTableName(j),
234                  rsMetaData.getColumnName(j),
235                  rsMetaData.getColumnLabel(j),
236                  rsMetaData.getColumnDisplaySize(j),
237                  rsMetaData.getColumnType(j),
238                  rsMetaData.getColumnTypeName(j),
239                  rsMetaData.getColumnClassName(j),
240                  rsMetaData.getPrecision(j),
241                  rsMetaData.getScale(j));
242             // @formatter:on
243         }
244 
245         private ResultSetColumnMetaData(final String schemaName, final String catalogName, final String tableName,
246                 final String name, final String label, final int displaySize, final int type, final String typeName,
247                 final String className, final int precision, final int scale) {
248             super();
249             this.schemaName = schemaName;
250             this.catalogName = catalogName;
251             this.tableName = tableName;
252             this.name = name;
253             this.nameKey = ColumnMapping.toKey(name);
254             this.label = label;
255             this.displaySize = displaySize;
256             this.type = type;
257             this.typeName = typeName;
258             this.className = className;
259             this.precision = precision;
260             this.scale = scale;
261             // TODO How about also using the className?
262             // @formatter:off
263             this.isStringType =
264                     type == Types.CHAR ||
265                     type == Types.LONGNVARCHAR ||
266                     type == Types.LONGVARCHAR ||
267                     type == Types.NVARCHAR ||
268                     type == Types.VARCHAR;
269             // @formatter:on
270         }
271 
272         public String getCatalogName() {
273             return catalogName;
274         }
275 
276         public String getClassName() {
277             return className;
278         }
279 
280         public int getDisplaySize() {
281             return displaySize;
282         }
283 
284         public String getLabel() {
285             return label;
286         }
287 
288         public String getName() {
289             return name;
290         }
291 
292         public String getNameKey() {
293             return nameKey;
294         }
295 
296         public int getPrecision() {
297             return precision;
298         }
299 
300         public int getScale() {
301             return scale;
302         }
303 
304         public String getSchemaName() {
305             return schemaName;
306         }
307 
308         public String getTableName() {
309             return tableName;
310         }
311 
312         public int getType() {
313             return type;
314         }
315 
316         public String getTypeName() {
317             return typeName;
318         }
319 
320         public boolean isStringType() {
321             return this.isStringType;
322         }
323 
324         @Override
325         public String toString() {
326             return String.format(
327                     "ColumnMetaData [schemaName=%s, catalogName=%s, tableName=%s, name=%s, nameKey=%s, label=%s, displaySize=%s, type=%s, typeName=%s, className=%s, precision=%s, scale=%s, isStringType=%s]",
328                     schemaName, catalogName, tableName, name, nameKey, label, displaySize, type, typeName, className,
329                     precision, scale, isStringType);
330         }
331 
332         public String truncate(final String string) {
333             return precision > 0 ? Strings.left(string, precision) : string;
334         }
335     }
336 
337     private static final JdbcDatabaseManagerFactory INSTANCE = new JdbcDatabaseManagerFactory();
338 
339     private static void appendColumnName(final int i, final String columnName, final StringBuilder sb) {
340         if (i > 1) {
341             sb.append(',');
342         }
343         sb.append(columnName);
344     }
345 
346     /**
347      * Appends column names to the given buffer in the format {@code "A,B,C"}.
348      */
349     private static void appendColumnNames(final String sqlVerb, final FactoryData data, final StringBuilder sb) {
350         // so this gets a little more complicated now that there are two ways to
351         // configure column mappings, but
352         // both mappings follow the same exact pattern for the prepared statement
353         int i = 1;
354         final String messagePattern = "Appending {} {}[{}]: {}={} ";
355         if (data.columnMappings != null) {
356             for (final ColumnMapping colMapping : data.columnMappings) {
357                 final String columnName = colMapping.getName();
358                 appendColumnName(i, columnName, sb);
359                 logger().trace(messagePattern, sqlVerb, colMapping.getClass().getSimpleName(), i, columnName,
360                         colMapping);
361                 i++;
362             }
363             if (data.columnConfigs != null) {
364                 for (final ColumnConfig colConfig : data.columnConfigs) {
365                     final String columnName = colConfig.getColumnName();
366                     appendColumnName(i, columnName, sb);
367                     logger().trace(messagePattern, sqlVerb, colConfig.getClass().getSimpleName(), i, columnName,
368                             colConfig);
369                     i++;
370                 }
371             }
372         }
373     }
374 
375     private static JdbcDatabaseManagerFactory getFactory() {
376         return INSTANCE;
377     }
378 
379     /**
380      * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
381      *
382      * @param name The name of the manager, which should include connection details and hashed passwords where possible.
383      * @param bufferSize The size of the log event buffer.
384      * @param connectionSource The source for connections to the database.
385      * @param tableName The name of the database table to insert log events into.
386      * @param columnConfigs Configuration information about the log table columns.
387      * @return a new or existing JDBC manager as applicable.
388      * @deprecated use
389      * {@link #getManager(String, int, Layout, ConnectionSource, String, ColumnConfig[], ColumnMapping[], boolean, long)}
390      */
391     @Deprecated
392     public static JdbcDatabaseManager getJDBCDatabaseManager(final String name, final int bufferSize,
393             final ConnectionSource connectionSource, final String tableName, final ColumnConfig[] columnConfigs) {
394         return getManager(
395                 name, new FactoryData(bufferSize, null, connectionSource, tableName, columnConfigs,
396                         new ColumnMapping[0], false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS, true),
397                 getFactory());
398     }
399 
400     /**
401      * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
402      *
403      * @param name The name of the manager, which should include connection details and hashed passwords where possible.
404      * @param bufferSize The size of the log event buffer.
405      * @param layout The Appender-level layout
406      * @param connectionSource The source for connections to the database.
407      * @param tableName The name of the database table to insert log events into.
408      * @param columnConfigs Configuration information about the log table columns.
409      * @param columnMappings column mapping configuration (including type conversion).
410      * @return a new or existing JDBC manager as applicable.
411      */
412     @Deprecated
413     public static JdbcDatabaseManager getManager(final String name, final int bufferSize,
414             final Layout<? extends Serializable> layout, final ConnectionSource connectionSource,
415             final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings) {
416         return getManager(name, new FactoryData(bufferSize, layout, connectionSource, tableName, columnConfigs,
417                 columnMappings, false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS, true), getFactory());
418     }
419 
420     /**
421      * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
422      *
423      * @param name The name of the manager, which should include connection details and hashed passwords where possible.
424      * @param bufferSize The size of the log event buffer.
425      * @param layout
426      * @param connectionSource The source for connections to the database.
427      * @param tableName The name of the database table to insert log events into.
428      * @param columnConfigs Configuration information about the log table columns.
429      * @param columnMappings column mapping configuration (including type conversion).
430      * @param reconnectIntervalMillis
431      * @param immediateFail
432      * @return a new or existing JDBC manager as applicable.
433      * @deprecated use
434      * {@link #getManager(String, int, Layout, ConnectionSource, String, ColumnConfig[], ColumnMapping[], boolean, long)}
435      */
436     @Deprecated
437     public static JdbcDatabaseManager getManager(final String name, final int bufferSize,
438             final Layout<? extends Serializable> layout, final ConnectionSource connectionSource,
439             final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings,
440             final boolean immediateFail, final long reconnectIntervalMillis) {
441         return getManager(name, new FactoryData(bufferSize, null, connectionSource, tableName, columnConfigs,
442                 columnMappings, false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS, true), getFactory());
443     }
444 
445     /**
446      * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
447      *
448      * @param name The name of the manager, which should include connection details and hashed passwords where possible.
449      * @param bufferSize The size of the log event buffer.
450      * @param layout The Appender-level layout
451      * @param connectionSource The source for connections to the database.
452      * @param tableName The name of the database table to insert log events into.
453      * @param columnConfigs Configuration information about the log table columns.
454      * @param columnMappings column mapping configuration (including type conversion).
455      * @param immediateFail Whether or not to fail immediately with a {@link AppenderLoggingException} when connecting
456      * to JDBC fails.
457      * @param reconnectIntervalMillis How often to reconnect to the database when a SQL exception is detected.
458      * @param truncateStrings Whether or not to truncate strings to match column metadata.
459      * @return a new or existing JDBC manager as applicable.
460      */
461     public static JdbcDatabaseManager getManager(final String name, final int bufferSize,
462             final Layout<? extends Serializable> layout, final ConnectionSource connectionSource,
463             final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings,
464             final boolean immediateFail, final long reconnectIntervalMillis, final boolean truncateStrings) {
465         return getManager(name, new FactoryData(bufferSize, layout, connectionSource, tableName, columnConfigs,
466                 columnMappings, immediateFail, reconnectIntervalMillis, truncateStrings), getFactory());
467     }
468 
469     // NOTE: prepared statements are prepared in this order: column mappings, then column configs
470     private final List<ColumnConfig> columnConfigs;
471     private final String sqlStatement;
472     private final FactoryData factoryData;
473     private volatile Connection connection;
474     private volatile PreparedStatement statement;
475     private volatile Reconnector reconnector;
476     private volatile boolean isBatchSupported;
477     private volatile Map<String, ResultSetColumnMetaData> columnMetaData;
478 
479     private JdbcDatabaseManager(final String name, final String sqlStatement, final List<ColumnConfig> columnConfigs,
480             final FactoryData factoryData) {
481         super(name, factoryData.getBufferSize());
482         this.sqlStatement = sqlStatement;
483         this.columnConfigs = columnConfigs;
484         this.factoryData = factoryData;
485     }
486 
487     private void checkConnection() {
488         boolean connClosed = true;
489         try {
490             connClosed = isClosed(this.connection);
491         } catch (final SQLException e) {
492             // Be quiet
493         }
494         boolean stmtClosed = true;
495         try {
496             stmtClosed = isClosed(this.statement);
497         } catch (final SQLException e) {
498             // Be quiet
499         }
500         if (!this.isRunning() || connClosed || stmtClosed) {
501             // If anything is closed, close it all down before we reconnect
502             closeResources(false);
503             // Reconnect
504             if (reconnector != null && !factoryData.immediateFail) {
505                 reconnector.latch();
506                 if (connection == null) {
507                     throw new AppenderLoggingException(
508                             "Error writing to JDBC Manager '%s': JDBC connection not available [%s]", getName(), fieldsToString());
509                 }
510                 if (statement == null) {
511                     throw new AppenderLoggingException(
512                             "Error writing to JDBC Manager '%s': JDBC statement not available [%s].", getName(), connection, fieldsToString());
513                 }
514             }
515         }
516     }
517 
518     protected void closeResources(final boolean logExceptions) {
519         final PreparedStatement tempPreparedStatement = this.statement;
520         this.statement = null;
521         try {
522             // Closing a statement returns it to the pool when using Apache Commons DBCP.
523             // Closing an already closed statement has no effect.
524             Closer.close(tempPreparedStatement);
525         } catch (final Exception e) {
526             if (logExceptions) {
527                 logWarn("Failed to close SQL statement logging event or flushing buffer", e);
528             }
529         }
530 
531         final Connection tempConnection = this.connection;
532         this.connection = null;
533         try {
534             // Closing a connection returns it to the pool when using Apache Commons DBCP.
535             // Closing an already closed connection has no effect.
536             Closer.close(tempConnection);
537         } catch (final Exception e) {
538             if (logExceptions) {
539                 logWarn("Failed to close database connection logging event or flushing buffer", e);
540             }
541         }
542     }
543 
544     @Override
545     protected boolean commitAndClose() {
546         final boolean closed = true;
547         try {
548             if (this.connection != null && !this.connection.isClosed()) {
549                 if (isBuffered() && this.isBatchSupported && this.statement != null) {
550                     logger().debug("Executing batch PreparedStatement {}", this.statement);
551                     int[] result;
552                     try {
553                         result = this.statement.executeBatch();
554                     } catch (SQLTransactionRollbackException e) {
555                         logger().debug("{} executing batch PreparedStatement {}, retrying.", e, this.statement);
556                         result = this.statement.executeBatch();
557                     }
558                     logger().debug("Batch result: {}", Arrays.toString(result));
559                 }
560                 logger().debug("Committing Connection {}", this.connection);
561                 this.connection.commit();
562             }
563         } catch (final SQLException e) {
564             throw new DbAppenderLoggingException(e, "Failed to commit transaction logging event or flushing buffer [%s]",
565                     fieldsToString());
566         } finally {
567             closeResources(true);
568         }
569         return closed;
570     }
571 
572     private boolean commitAndCloseAll() {
573         if (this.connection != null || this.statement != null) {
574             try {
575                 this.commitAndClose();
576                 return true;
577             } catch (final AppenderLoggingException e) {
578                 // Database connection has likely gone stale.
579                 final Throwable cause = e.getCause();
580                 final Throwable actual = cause == null ? e : cause;
581                 logger().debug("{} committing and closing connection: {}", actual, actual.getClass().getSimpleName(),
582                         e.toString(), e);
583             }
584         }
585         if (factoryData.connectionSource != null) {
586             factoryData.connectionSource.stop();
587         }
588         return true;
589     }
590 
591     private void connectAndPrepare() throws SQLException {
592         logger().debug("Acquiring JDBC connection from {}", this.getConnectionSource());
593         this.connection = getConnectionSource().getConnection();
594         logger().debug("Acquired JDBC connection {}", this.connection);
595         logger().debug("Getting connection metadata {}", this.connection);
596         final DatabaseMetaData databaseMetaData = this.connection.getMetaData();
597         logger().debug("Connection metadata {}", databaseMetaData);
598         this.isBatchSupported = databaseMetaData.supportsBatchUpdates();
599         logger().debug("Connection supportsBatchUpdates: {}", this.isBatchSupported);
600         this.connection.setAutoCommit(false);
601         logger().debug("Preparing SQL {}", this.sqlStatement);
602         this.statement = this.connection.prepareStatement(this.sqlStatement);
603         logger().debug("Prepared SQL {}", this.statement);
604         if (this.factoryData.truncateStrings) {
605             initColumnMetaData();
606         }
607     }
608 
609     @Override
610     protected void connectAndStart() {
611         checkConnection();
612         synchronized (this) {
613             try {
614                 connectAndPrepare();
615             } catch (final SQLException e) {
616                 reconnectOn(e);
617             }
618         }
619     }
620 
621     private Reconnector createReconnector() {
622         final Reconnector recon = new Reconnector();
623         recon.setDaemon(true);
624         recon.setPriority(Thread.MIN_PRIORITY);
625         return recon;
626     }
627 
628     private String createSqlSelect() {
629         final StringBuilder sb = new StringBuilder("select ");
630         appendColumnNames("SELECT", this.factoryData, sb);
631         sb.append(" from ");
632         sb.append(this.factoryData.tableName);
633         sb.append(" where 1=0");
634         return sb.toString();
635     }
636 
637     private String fieldsToString() {
638         return String.format(
639                 "columnConfigs=%s, sqlStatement=%s, factoryData=%s, connection=%s, statement=%s, reconnector=%s, isBatchSupported=%s, columnMetaData=%s",
640                 columnConfigs, sqlStatement, factoryData, connection, statement, reconnector, isBatchSupported,
641                 columnMetaData);
642     }
643 
644     public ConnectionSource getConnectionSource() {
645         return factoryData.connectionSource;
646     }
647 
648     public String getSqlStatement() {
649         return sqlStatement;
650     }
651 
652     public String getTableName() {
653         return factoryData.tableName;
654     }
655 
656     private void initColumnMetaData() throws SQLException {
657         // Could use:
658         // this.connection.getMetaData().getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern);
659         // But this returns more data than we need for now, so do a SQL SELECT with 0 result rows instead.
660         final String sqlSelect = createSqlSelect();
661         logger().debug("Getting SQL metadata for table {}: {}", this.factoryData.tableName, sqlSelect);
662         try (final PreparedStatement mdStatement = this.connection.prepareStatement(sqlSelect)) {
663             final ResultSetMetaData rsMetaData = mdStatement.getMetaData();
664             logger().debug("SQL metadata: {}", rsMetaData);
665             if (rsMetaData != null) {
666                 final int columnCount = rsMetaData.getColumnCount();
667                 columnMetaData = new HashMap<>(columnCount);
668                 for (int i = 0, j = 1; i < columnCount; i++, j++) {
669                     final ResultSetColumnMetaData value = new ResultSetColumnMetaData(rsMetaData, j);
670                     columnMetaData.put(value.getNameKey(), value);
671                 }
672             } else {
673                 logger().warn(
674                         "{}: truncateStrings is true and ResultSetMetaData is null for statement: {}; manager will not perform truncation.",
675                         getClass().getSimpleName(), mdStatement);
676             }
677         }
678     }
679 
680     /**
681      * Checks if a statement is closed. A null statement is considered closed.
682      *
683      * @param statement The statement to check.
684      * @return true if a statement is closed, false if null.
685      * @throws SQLException if a database access error occurs
686      */
687     private boolean isClosed(final Statement statement) throws SQLException {
688         return statement == null || statement.isClosed();
689     }
690 
691     /**
692      * Checks if a connection is closed. A null connection is considered closed.
693      *
694      * @param connection The connection to check.
695      * @return true if a connection is closed, false if null.
696      * @throws SQLException if a database access error occurs
697      */
698     private boolean isClosed(final Connection connection) throws SQLException {
699         return connection == null || connection.isClosed();
700     }
701 
702     private void reconnectOn(final Exception exception) {
703         if (!factoryData.retry) {
704             throw new AppenderLoggingException("Cannot connect and prepare", exception);
705         }
706         if (reconnector == null) {
707             reconnector = createReconnector();
708             try {
709                 reconnector.reconnect();
710             } catch (final SQLException reconnectEx) {
711                 logger().debug("Cannot reestablish JDBC connection to {}: {}; starting reconnector thread {}",
712                         factoryData, reconnectEx, reconnector.getName(), reconnectEx);
713                 reconnector.start();
714                 reconnector.latch();
715                 if (connection == null || statement == null) {
716                     throw new AppenderLoggingException(exception, "Error sending to %s for %s [%s]", getName(),
717                             factoryData, fieldsToString());
718                 }
719             }
720         }
721     }
722 
723     private void setFields(final MapMessage<?, ?> mapMessage) throws SQLException {
724         final IndexedReadOnlyStringMap map = mapMessage.getIndexedReadOnlyStringMap();
725         final String simpleName = statement.getClass().getName();
726         int j = 1; // JDBC indices start at 1
727         if (this.factoryData.columnMappings != null) {
728             for (final ColumnMapping mapping : this.factoryData.columnMappings) {
729                 if (mapping.getLiteralValue() == null) {
730                     final String source = mapping.getSource();
731                     final String key = Strings.isEmpty(source) ? mapping.getName() : source;
732                     final Object value = map.getValue(key);
733                     if (logger().isTraceEnabled()) {
734                         final String valueStr = value instanceof String ? "\"" + value + "\""
735                                 : Objects.toString(value, null);
736                         logger().trace("{} setObject({}, {}) for key '{}' and mapping '{}'", simpleName, j, valueStr,
737                                 key, mapping.getName());
738                     }
739                     setStatementObject(j, mapping.getNameKey(), value);
740                     j++;
741                 }
742             }
743         }
744     }
745 
746     /**
747      * Sets the given Object in the prepared statement. The value is truncated if needed.
748      */
749     private void setStatementObject(final int j, final String nameKey, final Object value) throws SQLException {
750         statement.setObject(j, truncate(nameKey, value));
751     }
752 
753     @Override
754     protected boolean shutdownInternal() {
755         if (reconnector != null) {
756             reconnector.shutdown();
757             reconnector.interrupt();
758             reconnector = null;
759         }
760         return commitAndCloseAll();
761     }
762 
763     @Override
764     protected void startupInternal() throws Exception {
765         // empty
766     }
767 
768     /**
769      * Truncates the value if needed.
770      */
771     private Object truncate(final String nameKey, Object value) {
772         if (value != null && this.factoryData.truncateStrings && columnMetaData != null) {
773             final ResultSetColumnMetaData resultSetColumnMetaData = columnMetaData.get(nameKey);
774             if (resultSetColumnMetaData != null) {
775                 if (resultSetColumnMetaData.isStringType()) {
776                     value = resultSetColumnMetaData.truncate(value.toString());
777                 }
778             } else {
779                 logger().error("Missing ResultSetColumnMetaData for {}, connection={}, statement={}", nameKey,
780                         connection, statement);
781             }
782         }
783         return value;
784     }
785 
786     @Override
787     protected void writeInternal(final LogEvent event, final Serializable serializable) {
788         StringReader reader = null;
789         try {
790             if (!this.isRunning() || isClosed(this.connection) || isClosed(this.statement)) {
791                 throw new AppenderLoggingException(
792                         "Cannot write logging event; JDBC manager not connected to the database, running=%s, [%s]).",
793                         isRunning(), fieldsToString());
794             }
795             // Clear in case there are leftovers.
796             statement.clearParameters();
797             if (serializable instanceof MapMessage) {
798                 setFields((MapMessage<?, ?>) serializable);
799             }
800             int j = 1; // JDBC indices start at 1
801             if (this.factoryData.columnMappings != null) {
802                 for (final ColumnMapping mapping : this.factoryData.columnMappings) {
803                     if (ThreadContextMap.class.isAssignableFrom(mapping.getType())
804                             || ReadOnlyStringMap.class.isAssignableFrom(mapping.getType())) {
805                         this.statement.setObject(j++, event.getContextData().toMap());
806                     } else if (ThreadContextStack.class.isAssignableFrom(mapping.getType())) {
807                         this.statement.setObject(j++, event.getContextStack().asList());
808                     } else if (Date.class.isAssignableFrom(mapping.getType())) {
809                         this.statement.setObject(j++, DateTypeConverter.fromMillis(event.getTimeMillis(),
810                                 mapping.getType().asSubclass(Date.class)));
811                     } else {
812                         final StringLayout layout = mapping.getLayout();
813                         if (layout != null) {
814                             if (Clob.class.isAssignableFrom(mapping.getType())) {
815                                 this.statement.setClob(j++, new StringReader(layout.toSerializable(event)));
816                             } else if (NClob.class.isAssignableFrom(mapping.getType())) {
817                                 this.statement.setNClob(j++, new StringReader(layout.toSerializable(event)));
818                             } else {
819                                 final Object value = TypeConverters.convert(layout.toSerializable(event),
820                                         mapping.getType(), null);
821                                 if (value == null) {
822                                     // TODO We might need to always initialize the columnMetaData to specify the
823                                     // type.
824                                     this.statement.setNull(j++, Types.NULL);
825                                 } else {
826                                     setStatementObject(j++, mapping.getNameKey(), value);
827                                 }
828                             }
829                         }
830                     }
831                 }
832             }
833             for (final ColumnConfig column : this.columnConfigs) {
834                 if (column.isEventTimestamp()) {
835                     this.statement.setTimestamp(j++, new Timestamp(event.getTimeMillis()));
836                 } else if (column.isClob()) {
837                     reader = new StringReader(column.getLayout().toSerializable(event));
838                     if (column.isUnicode()) {
839                         this.statement.setNClob(j++, reader);
840                     } else {
841                         this.statement.setClob(j++, reader);
842                     }
843                 } else if (column.isUnicode()) {
844                     this.statement.setNString(j++, Objects.toString(
845                             truncate(column.getColumnNameKey(), column.getLayout().toSerializable(event)), null));
846                 } else {
847                     this.statement.setString(j++, Objects.toString(
848                             truncate(column.getColumnNameKey(), column.getLayout().toSerializable(event)), null));
849                 }
850             }
851 
852             if (isBuffered() && this.isBatchSupported) {
853                 this.statement.addBatch();
854             } else if (this.statement.executeUpdate() == 0) {
855                 throw new AppenderLoggingException(
856                         "No records inserted in database table for log event in JDBC manager [%s].", fieldsToString());
857             }
858         } catch (final SQLException e) {
859             throw new DbAppenderLoggingException(e, "Failed to insert record for log event in JDBC manager: %s [%s]", e,
860                     fieldsToString());
861         } finally {
862             // Release ASAP
863             try {
864                 // statement can be null when a AppenderLoggingException is thrown at the start of this method
865                 if (statement != null) {
866                     statement.clearParameters();
867                 }
868             } catch (final SQLException e) {
869                 // Ignore
870             }
871             Closer.closeSilently(reader);
872         }
873     }
874 
875     @Override
876     protected void writeThrough(final LogEvent event, final Serializable serializable) {
877         this.connectAndStart();
878         try {
879             try {
880                 this.writeInternal(event, serializable);
881             } finally {
882                 this.commitAndClose();
883             }
884         } catch (final DbAppenderLoggingException e) {
885             reconnectOn(e);
886             try {
887                 this.writeInternal(event, serializable);
888             } finally {
889                 this.commitAndClose();
890             }
891         }
892     }
893 
894 }