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