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