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