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