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.StringReader; 020import java.sql.Clob; 021import java.sql.Connection; 022import java.sql.DatabaseMetaData; 023import java.sql.NClob; 024import java.sql.PreparedStatement; 025import java.sql.SQLException; 026import java.sql.Timestamp; 027import java.sql.Types; 028import java.util.ArrayList; 029import java.util.Date; 030import java.util.List; 031 032import org.apache.logging.log4j.core.LogEvent; 033import org.apache.logging.log4j.core.appender.AppenderLoggingException; 034import org.apache.logging.log4j.core.appender.ManagerFactory; 035import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager; 036import org.apache.logging.log4j.core.appender.db.ColumnMapping; 037import org.apache.logging.log4j.core.config.plugins.convert.DateTypeConverter; 038import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters; 039import org.apache.logging.log4j.core.util.Closer; 040import org.apache.logging.log4j.spi.ThreadContextMap; 041import org.apache.logging.log4j.spi.ThreadContextStack; 042import org.apache.logging.log4j.util.ReadOnlyStringMap; 043import org.apache.logging.log4j.util.Strings; 044 045/** 046 * An {@link AbstractDatabaseManager} implementation for relational databases accessed via JDBC. 047 */ 048public final class JdbcDatabaseManager extends AbstractDatabaseManager { 049 050 private static final JdbcDatabaseManagerFactory INSTANCE = new JdbcDatabaseManagerFactory(); 051 052 // NOTE: prepared statements are prepared in this order: column mappings, then column configs 053 private final List<ColumnMapping> columnMappings; 054 private final List<ColumnConfig> columnConfigs; 055 private final ConnectionSource connectionSource; 056 private final String sqlStatement; 057 058 private Connection connection; 059 private PreparedStatement statement; 060 private boolean isBatchSupported; 061 062 private JdbcDatabaseManager(final String name, final int bufferSize, final ConnectionSource connectionSource, 063 final String sqlStatement, final List<ColumnConfig> columnConfigs, 064 final List<ColumnMapping> columnMappings) { 065 super(name, bufferSize); 066 this.connectionSource = connectionSource; 067 this.sqlStatement = sqlStatement; 068 this.columnConfigs = columnConfigs; 069 this.columnMappings = columnMappings; 070 } 071 072 @Override 073 protected void startupInternal() throws Exception { 074 this.connection = this.connectionSource.getConnection(); 075 final DatabaseMetaData metaData = this.connection.getMetaData(); 076 this.isBatchSupported = metaData.supportsBatchUpdates(); 077 Closer.closeSilently(this.connection); 078 } 079 080 @Override 081 protected boolean shutdownInternal() { 082 if (this.connection != null || this.statement != null) { 083 return this.commitAndClose(); 084 } 085 return true; 086 } 087 088 @Override 089 protected void connectAndStart() { 090 try { 091 this.connection = this.connectionSource.getConnection(); 092 this.connection.setAutoCommit(false); 093 this.statement = this.connection.prepareStatement(this.sqlStatement); 094 } catch (final SQLException e) { 095 throw new AppenderLoggingException( 096 "Cannot write logging event or flush buffer; JDBC manager cannot connect to the database.", e 097 ); 098 } 099 } 100 101 @Override 102 protected void writeInternal(final LogEvent event) { 103 StringReader reader = null; 104 try { 105 if (!this.isRunning() || this.connection == null || this.connection.isClosed() || this.statement == null 106 || this.statement.isClosed()) { 107 throw new AppenderLoggingException( 108 "Cannot write logging event; JDBC manager not connected to the database."); 109 } 110 111 int i = 1; 112 for (final ColumnMapping mapping : this.columnMappings) { 113 if (ThreadContextMap.class.isAssignableFrom(mapping.getType()) 114 || ReadOnlyStringMap.class.isAssignableFrom(mapping.getType())) { 115 this.statement.setObject(i++, event.getContextData().toMap()); 116 } else if (ThreadContextStack.class.isAssignableFrom(mapping.getType())) { 117 this.statement.setObject(i++, event.getContextStack().asList()); 118 } else if (Date.class.isAssignableFrom(mapping.getType())) { 119 this.statement.setObject(i++, 120 DateTypeConverter.fromMillis(event.getTimeMillis(), mapping.getType().asSubclass(Date.class))); 121 } else if (Clob.class.isAssignableFrom(mapping.getType())) { 122 this.statement.setClob(i++, new StringReader(mapping.getLayout().toSerializable(event))); 123 } else if (NClob.class.isAssignableFrom(mapping.getType())) { 124 this.statement.setNClob(i++, new StringReader(mapping.getLayout().toSerializable(event))); 125 } else { 126 final Object value = TypeConverters.convert(mapping.getLayout().toSerializable(event), 127 mapping.getType(), null); 128 if (value == null) { 129 this.statement.setNull(i++, Types.NULL); 130 } else { 131 this.statement.setObject(i++, value); 132 } 133 } 134 } 135 for (final ColumnConfig column : this.columnConfigs) { 136 if (column.isEventTimestamp()) { 137 this.statement.setTimestamp(i++, new Timestamp(event.getTimeMillis())); 138 } else if (column.isClob()) { 139 reader = new StringReader(column.getLayout().toSerializable(event)); 140 if (column.isUnicode()) { 141 this.statement.setNClob(i++, reader); 142 } else { 143 this.statement.setClob(i++, reader); 144 } 145 } else if (column.isUnicode()) { 146 this.statement.setNString(i++, column.getLayout().toSerializable(event)); 147 } else { 148 this.statement.setString(i++, column.getLayout().toSerializable(event)); 149 } 150 } 151 152 if (this.isBatchSupported) { 153 this.statement.addBatch(); 154 } else if (this.statement.executeUpdate() == 0) { 155 throw new AppenderLoggingException( 156 "No records inserted in database table for log event in JDBC manager."); 157 } 158 } catch (final SQLException e) { 159 throw new AppenderLoggingException("Failed to insert record for log event in JDBC manager: " + 160 e.getMessage(), e); 161 } finally { 162 Closer.closeSilently(reader); 163 } 164 } 165 166 @Override 167 protected boolean commitAndClose() { 168 boolean closed = true; 169 try { 170 if (this.connection != null && !this.connection.isClosed()) { 171 if (this.isBatchSupported) { 172 this.statement.executeBatch(); 173 } 174 this.connection.commit(); 175 } 176 } catch (final SQLException e) { 177 throw new AppenderLoggingException("Failed to commit transaction logging event or flushing buffer.", e); 178 } finally { 179 try { 180 Closer.close(this.statement); 181 } catch (final Exception e) { 182 logWarn("Failed to close SQL statement logging event or flushing buffer", e); 183 closed = false; 184 } finally { 185 this.statement = null; 186 } 187 188 try { 189 Closer.close(this.connection); 190 } catch (final Exception e) { 191 logWarn("Failed to close database connection logging event or flushing buffer", e); 192 closed = false; 193 } finally { 194 this.connection = null; 195 } 196 } 197 return closed; 198 } 199 200 /** 201 * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists. 202 * 203 * @param name The name of the manager, which should include connection details and hashed passwords where possible. 204 * @param bufferSize The size of the log event buffer. 205 * @param connectionSource The source for connections to the database. 206 * @param tableName The name of the database table to insert log events into. 207 * @param columnConfigs Configuration information about the log table columns. 208 * @return a new or existing JDBC manager as applicable. 209 * @deprecated use {@link #getManager(String, int, ConnectionSource, String, ColumnConfig[], ColumnMapping[])} 210 */ 211 @Deprecated 212 public static JdbcDatabaseManager getJDBCDatabaseManager(final String name, final int bufferSize, 213 final ConnectionSource connectionSource, 214 final String tableName, 215 final ColumnConfig[] columnConfigs) { 216 217 return getManager(name, 218 new FactoryData(bufferSize, connectionSource, tableName, columnConfigs, new ColumnMapping[0]), 219 getFactory()); 220 } 221 222 /** 223 * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists. 224 * 225 * @param name The name of the manager, which should include connection details and hashed passwords where possible. 226 * @param bufferSize The size of the log event buffer. 227 * @param connectionSource The source for connections to the database. 228 * @param tableName The name of the database table to insert log events into. 229 * @param columnConfigs Configuration information about the log table columns. 230 * @param columnMappings column mapping configuration (including type conversion). 231 * @return a new or existing JDBC manager as applicable. 232 */ 233 public static JdbcDatabaseManager getManager(final String name, 234 final int bufferSize, 235 final ConnectionSource connectionSource, 236 final String tableName, 237 final ColumnConfig[] columnConfigs, 238 final ColumnMapping[] columnMappings) { 239 return getManager(name, new FactoryData(bufferSize, connectionSource, tableName, columnConfigs, columnMappings), 240 getFactory()); 241 } 242 243 private static JdbcDatabaseManagerFactory getFactory() { 244 return INSTANCE; 245 } 246 247 /** 248 * Encapsulates data that {@link JdbcDatabaseManagerFactory} uses to create managers. 249 */ 250 private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData { 251 private final ConnectionSource connectionSource; 252 private final String tableName; 253 private final ColumnConfig[] columnConfigs; 254 private final ColumnMapping[] columnMappings; 255 256 protected FactoryData(final int bufferSize, final ConnectionSource connectionSource, final String tableName, 257 final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings) { 258 super(bufferSize); 259 this.connectionSource = connectionSource; 260 this.tableName = tableName; 261 this.columnConfigs = columnConfigs; 262 this.columnMappings = columnMappings; 263 } 264 } 265 266 /** 267 * Creates managers. 268 */ 269 private static final class JdbcDatabaseManagerFactory implements ManagerFactory<JdbcDatabaseManager, FactoryData> { 270 @Override 271 public JdbcDatabaseManager createManager(final String name, final FactoryData data) { 272 final StringBuilder sb = new StringBuilder("INSERT INTO ").append(data.tableName).append(" ("); 273 // so this gets a little more complicated now that there are two ways to configure column mappings, but 274 // both mappings follow the same exact pattern for the prepared statement 275 for (final ColumnMapping mapping : data.columnMappings) { 276 sb.append(mapping.getName()).append(','); 277 } 278 for (final ColumnConfig config : data.columnConfigs) { 279 sb.append(config.getColumnName()).append(','); 280 } 281 // at least one of those arrays is guaranteed to be non-empty 282 sb.setCharAt(sb.length() - 1, ')'); 283 sb.append(" VALUES ("); 284 final List<ColumnMapping> columnMappings = new ArrayList<>(data.columnMappings.length); 285 for (final ColumnMapping mapping : data.columnMappings) { 286 if (Strings.isNotEmpty(mapping.getLiteralValue())) { 287 sb.append(mapping.getLiteralValue()); 288 } else { 289 sb.append('?'); 290 columnMappings.add(mapping); 291 } 292 sb.append(','); 293 } 294 final List<ColumnConfig> columnConfigs = new ArrayList<>(data.columnConfigs.length); 295 for (final ColumnConfig config : data.columnConfigs) { 296 if (Strings.isNotEmpty(config.getLiteralValue())) { 297 sb.append(config.getLiteralValue()); 298 } else { 299 sb.append('?'); 300 columnConfigs.add(config); 301 } 302 sb.append(','); 303 } 304 // at least one of those arrays is guaranteed to be non-empty 305 sb.setCharAt(sb.length() - 1, ')'); 306 final String sqlStatement = sb.toString(); 307 308 return new JdbcDatabaseManager(name, data.getBufferSize(), data.connectionSource, sqlStatement, 309 columnConfigs, columnMappings); 310 } 311 } 312 313}