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