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.Connection; 021import java.sql.DatabaseMetaData; 022import java.sql.PreparedStatement; 023import java.sql.SQLException; 024import java.sql.Timestamp; 025import java.util.ArrayList; 026import java.util.List; 027 028import org.apache.logging.log4j.core.LogEvent; 029import org.apache.logging.log4j.core.appender.AppenderLoggingException; 030import org.apache.logging.log4j.core.appender.ManagerFactory; 031import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager; 032import org.apache.logging.log4j.core.layout.PatternLayout; 033import org.apache.logging.log4j.core.util.Closer; 034 035/** 036 * An {@link AbstractDatabaseManager} implementation for relational databases accessed via JDBC. 037 */ 038public final class JdbcDatabaseManager extends AbstractDatabaseManager { 039 040 private static final JdbcDatabaseManagerFactory INSTANCE = new JdbcDatabaseManagerFactory(); 041 042 private final List<Column> columns; 043 private final ConnectionSource connectionSource; 044 private final String sqlStatement; 045 046 private Connection connection; 047 private PreparedStatement statement; 048 private boolean isBatchSupported; 049 050 private JdbcDatabaseManager(final String name, final int bufferSize, final ConnectionSource connectionSource, 051 final String sqlStatement, final List<Column> columns) { 052 super(name, bufferSize); 053 this.connectionSource = connectionSource; 054 this.sqlStatement = sqlStatement; 055 this.columns = columns; 056 } 057 058 @Override 059 protected void startupInternal() throws Exception { 060 this.connection = this.connectionSource.getConnection(); 061 final DatabaseMetaData metaData = this.connection.getMetaData(); 062 this.isBatchSupported = metaData.supportsBatchUpdates(); 063 Closer.closeSilently(this.connection); 064 } 065 066 @Override 067 protected void shutdownInternal() { 068 if (this.connection != null || this.statement != null) { 069 this.commitAndClose(); 070 } 071 } 072 073 @Override 074 protected void connectAndStart() { 075 try { 076 this.connection = this.connectionSource.getConnection(); 077 this.connection.setAutoCommit(false); 078 this.statement = this.connection.prepareStatement(this.sqlStatement); 079 } catch (final SQLException e) { 080 throw new AppenderLoggingException( 081 "Cannot write logging event or flush buffer; JDBC manager cannot connect to the database.", e 082 ); 083 } 084 } 085 086 @Override 087 protected void writeInternal(final LogEvent event) { 088 StringReader reader = null; 089 try { 090 if (!this.isRunning() || this.connection == null || this.connection.isClosed() || this.statement == null 091 || this.statement.isClosed()) { 092 throw new AppenderLoggingException( 093 "Cannot write logging event; JDBC manager not connected to the database."); 094 } 095 096 int i = 1; 097 for (final Column column : this.columns) { 098 if (column.isEventTimestamp) { 099 this.statement.setTimestamp(i++, new Timestamp(event.getTimeMillis())); 100 } else { 101 if (column.isClob) { 102 reader = new StringReader(column.layout.toSerializable(event)); 103 if (column.isUnicode) { 104 this.statement.setNClob(i++, reader); 105 } else { 106 this.statement.setClob(i++, reader); 107 } 108 } else { 109 if (column.isUnicode) { 110 this.statement.setNString(i++, column.layout.toSerializable(event)); 111 } else { 112 this.statement.setString(i++, column.layout.toSerializable(event)); 113 } 114 } 115 } 116 } 117 118 if (this.isBatchSupported) { 119 this.statement.addBatch(); 120 } else if (this.statement.executeUpdate() == 0) { 121 throw new AppenderLoggingException( 122 "No records inserted in database table for log event in JDBC manager."); 123 } 124 } catch (final SQLException e) { 125 throw new AppenderLoggingException("Failed to insert record for log event in JDBC manager: " + 126 e.getMessage(), e); 127 } finally { 128 Closer.closeSilently(reader); 129 } 130 } 131 132 @Override 133 protected void commitAndClose() { 134 try { 135 if (this.connection != null && !this.connection.isClosed()) { 136 if (this.isBatchSupported) { 137 this.statement.executeBatch(); 138 } 139 this.connection.commit(); 140 } 141 } catch (final SQLException e) { 142 throw new AppenderLoggingException("Failed to commit transaction logging event or flushing buffer.", e); 143 } finally { 144 try { 145 Closer.close(this.statement); 146 } catch (final Exception e) { 147 LOGGER.warn("Failed to close SQL statement logging event or flushing buffer.", e); 148 } finally { 149 this.statement = null; 150 } 151 152 try { 153 Closer.close(this.connection); 154 } catch (final Exception e) { 155 LOGGER.warn("Failed to close database connection logging event or flushing buffer.", e); 156 } finally { 157 this.connection = null; 158 } 159 } 160 } 161 162 /** 163 * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists. 164 * 165 * @param name The name of the manager, which should include connection details and hashed passwords where possible. 166 * @param bufferSize The size of the log event buffer. 167 * @param connectionSource The source for connections to the database. 168 * @param tableName The name of the database table to insert log events into. 169 * @param columnConfigs Configuration information about the log table columns. 170 * @return a new or existing JDBC manager as applicable. 171 */ 172 public static JdbcDatabaseManager getJDBCDatabaseManager(final String name, final int bufferSize, 173 final ConnectionSource connectionSource, 174 final String tableName, 175 final ColumnConfig[] columnConfigs) { 176 177 return AbstractDatabaseManager.getManager( 178 name, new FactoryData(bufferSize, connectionSource, tableName, columnConfigs), getFactory() 179 ); 180 } 181 182 private static JdbcDatabaseManagerFactory getFactory() { 183 return INSTANCE; 184 } 185 186 /** 187 * Encapsulates data that {@link JdbcDatabaseManagerFactory} uses to create managers. 188 */ 189 private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData { 190 private final ColumnConfig[] columnConfigs; 191 private final ConnectionSource connectionSource; 192 private final String tableName; 193 194 protected FactoryData(final int bufferSize, final ConnectionSource connectionSource, final String tableName, 195 final ColumnConfig[] columnConfigs) { 196 super(bufferSize); 197 this.connectionSource = connectionSource; 198 this.tableName = tableName; 199 this.columnConfigs = columnConfigs; 200 } 201 } 202 203 /** 204 * Creates managers. 205 */ 206 private static final class JdbcDatabaseManagerFactory implements ManagerFactory<JdbcDatabaseManager, FactoryData> { 207 @Override 208 public JdbcDatabaseManager createManager(final String name, final FactoryData data) { 209 final StringBuilder columnPart = new StringBuilder(); 210 final StringBuilder valuePart = new StringBuilder(); 211 final List<Column> columns = new ArrayList<Column>(); 212 int i = 0; 213 for (final ColumnConfig config : data.columnConfigs) { 214 if (i++ > 0) { 215 columnPart.append(','); 216 valuePart.append(','); 217 } 218 219 columnPart.append(config.getColumnName()); 220 221 if (config.getLiteralValue() != null) { 222 valuePart.append(config.getLiteralValue()); 223 } else { 224 columns.add(new Column( 225 config.getLayout(), config.isEventTimestamp(), config.isUnicode(), config.isClob() 226 )); 227 valuePart.append('?'); 228 } 229 } 230 231 final String sqlStatement = "INSERT INTO " + data.tableName + " (" + columnPart + ") VALUES (" + 232 valuePart + ')'; 233 234 return new JdbcDatabaseManager(name, data.getBufferSize(), data.connectionSource, sqlStatement, columns); 235 } 236 } 237 238 /** 239 * Encapsulates information about a database column and how to persist data to it. 240 */ 241 private static final class Column { 242 private final PatternLayout layout; 243 private final boolean isEventTimestamp; 244 private final boolean isUnicode; 245 private final boolean isClob; 246 247 private Column(final PatternLayout layout, final boolean isEventDate, final boolean isUnicode, 248 final boolean isClob) { 249 this.layout = layout; 250 this.isEventTimestamp = isEventDate; 251 this.isUnicode = isUnicode; 252 this.isClob = isClob; 253 } 254 } 255}