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.ResultSetMetaData; 027import java.sql.SQLException; 028import java.sql.SQLTransactionRollbackException; 029import java.sql.Statement; 030import java.sql.Timestamp; 031import java.sql.Types; 032import java.util.ArrayList; 033import java.util.Arrays; 034import java.util.Date; 035import java.util.HashMap; 036import java.util.List; 037import java.util.Map; 038import java.util.Objects; 039import java.util.concurrent.CountDownLatch; 040 041import org.apache.logging.log4j.core.Layout; 042import org.apache.logging.log4j.core.LogEvent; 043import org.apache.logging.log4j.core.StringLayout; 044import org.apache.logging.log4j.core.appender.AppenderLoggingException; 045import org.apache.logging.log4j.core.appender.ManagerFactory; 046import org.apache.logging.log4j.core.appender.db.AbstractDatabaseAppender; 047import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager; 048import org.apache.logging.log4j.core.appender.db.ColumnMapping; 049import org.apache.logging.log4j.core.appender.db.DbAppenderLoggingException; 050import org.apache.logging.log4j.core.config.plugins.convert.DateTypeConverter; 051import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters; 052import org.apache.logging.log4j.core.util.Closer; 053import org.apache.logging.log4j.core.util.Log4jThread; 054import org.apache.logging.log4j.message.MapMessage; 055import org.apache.logging.log4j.spi.ThreadContextMap; 056import org.apache.logging.log4j.spi.ThreadContextStack; 057import org.apache.logging.log4j.util.IndexedReadOnlyStringMap; 058import org.apache.logging.log4j.util.ReadOnlyStringMap; 059import org.apache.logging.log4j.util.Strings; 060 061/** 062 * An {@link AbstractDatabaseManager} implementation for relational databases accessed via JDBC. 063 */ 064public final class JdbcDatabaseManager extends AbstractDatabaseManager { 065 066 /** 067 * Encapsulates data that {@link JdbcDatabaseManagerFactory} uses to create managers. 068 */ 069 private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData { 070 private final ConnectionSource connectionSource; 071 private final String tableName; 072 private final ColumnConfig[] columnConfigs; 073 private final ColumnMapping[] columnMappings; 074 private final boolean immediateFail; 075 private final boolean retry; 076 private final long reconnectIntervalMillis; 077 private final boolean truncateStrings; 078 079 protected FactoryData(final int bufferSize, final Layout<? extends Serializable> layout, 080 final ConnectionSource connectionSource, final String tableName, final ColumnConfig[] columnConfigs, 081 final ColumnMapping[] columnMappings, final boolean immediateFail, final long reconnectIntervalMillis, 082 final boolean truncateStrings) { 083 super(bufferSize, layout); 084 this.connectionSource = connectionSource; 085 this.tableName = tableName; 086 this.columnConfigs = columnConfigs; 087 this.columnMappings = columnMappings; 088 this.immediateFail = immediateFail; 089 this.retry = reconnectIntervalMillis > 0; 090 this.reconnectIntervalMillis = reconnectIntervalMillis; 091 this.truncateStrings = truncateStrings; 092 } 093 094 @Override 095 public String toString() { 096 return String.format( 097 "FactoryData [connectionSource=%s, tableName=%s, columnConfigs=%s, columnMappings=%s, immediateFail=%s, retry=%s, reconnectIntervalMillis=%s, truncateStrings=%s]", 098 connectionSource, tableName, Arrays.toString(columnConfigs), Arrays.toString(columnMappings), 099 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}