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 (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.closeSilent(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 (SQLException e) { 131 throw new AppenderLoggingException("Failed to commit transaction logging event or flushing buffer.", e); 132 } finally { 133 try { 134 if (this.statement != null) { 135 this.statement.close(); 136 } 137 } catch (Exception e) { 138 LOGGER.warn("Failed to close SQL statement logging event or flushing buffer.", e); 139 } finally { 140 this.statement = null; 141 } 142 143 try { 144 if (this.connection != null) { 145 this.connection.close(); 146 } 147 } catch (Exception e) { 148 LOGGER.warn("Failed to close database connection logging event or flushing buffer.", e); 149 } finally { 150 this.connection = null; 151 } 152 } 153 } 154 155 /** 156 * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists. 157 * 158 * @param name The name of the manager, which should include connection details and hashed passwords where possible. 159 * @param bufferSize The size of the log event buffer. 160 * @param connectionSource The source for connections to the database. 161 * @param tableName The name of the database table to insert log events into. 162 * @param columnConfigs Configuration information about the log table columns. 163 * @return a new or existing JDBC manager as applicable. 164 */ 165 public static JdbcDatabaseManager getJDBCDatabaseManager(final String name, final int bufferSize, 166 final ConnectionSource connectionSource, 167 final String tableName, 168 final ColumnConfig[] columnConfigs) { 169 170 return AbstractDatabaseManager.getManager( 171 name, new FactoryData(bufferSize, connectionSource, tableName, columnConfigs), FACTORY 172 ); 173 } 174 175 /** 176 * Encapsulates data that {@link JDBCDatabaseManagerFactory} uses to create managers. 177 */ 178 private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData { 179 private final ColumnConfig[] columnConfigs; 180 private final ConnectionSource connectionSource; 181 private final String tableName; 182 183 protected FactoryData(final int bufferSize, final ConnectionSource connectionSource, final String tableName, 184 final ColumnConfig[] columnConfigs) { 185 super(bufferSize); 186 this.connectionSource = connectionSource; 187 this.tableName = tableName; 188 this.columnConfigs = columnConfigs; 189 } 190 } 191 192 /** 193 * Creates managers. 194 */ 195 private static final class JDBCDatabaseManagerFactory implements ManagerFactory<JdbcDatabaseManager, FactoryData> { 196 @Override 197 public JdbcDatabaseManager createManager(final String name, final FactoryData data) { 198 final StringBuilder columnPart = new StringBuilder(); 199 final StringBuilder valuePart = new StringBuilder(); 200 final List<Column> columns = new ArrayList<Column>(); 201 int i = 0; 202 for (final ColumnConfig config : data.columnConfigs) { 203 if (i++ > 0) { 204 columnPart.append(','); 205 valuePart.append(','); 206 } 207 208 columnPart.append(config.getColumnName()); 209 210 if (config.getLiteralValue() != null) { 211 valuePart.append(config.getLiteralValue()); 212 } else { 213 columns.add(new Column( 214 config.getLayout(), config.isEventTimestamp(), config.isUnicode(), config.isClob() 215 )); 216 valuePart.append('?'); 217 } 218 } 219 220 final String sqlStatement = "INSERT INTO " + data.tableName + " (" + columnPart + ") VALUES (" + 221 valuePart + ')'; 222 223 return new JdbcDatabaseManager(name, data.getBufferSize(), data.connectionSource, sqlStatement, columns); 224 } 225 } 226 227 /** 228 * Encapsulates information about a database column and how to persist data to it. 229 */ 230 private static final class Column { 231 private final PatternLayout layout; 232 private final boolean isEventTimestamp; 233 private final boolean isUnicode; 234 private final boolean isClob; 235 236 private Column(final PatternLayout layout, final boolean isEventDate, final boolean isUnicode, 237 final boolean isClob) { 238 this.layout = layout; 239 this.isEventTimestamp = isEventDate; 240 this.isUnicode = isUnicode; 241 this.isClob = isClob; 242 } 243 } 244 }