View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.appender.db.jdbc;
18  
19  import java.io.StringReader;
20  import java.sql.Clob;
21  import java.sql.Connection;
22  import java.sql.DatabaseMetaData;
23  import java.sql.NClob;
24  import java.sql.PreparedStatement;
25  import java.sql.SQLException;
26  import java.sql.Timestamp;
27  import java.sql.Types;
28  import java.util.ArrayList;
29  import java.util.Date;
30  import java.util.List;
31  
32  import org.apache.logging.log4j.core.LogEvent;
33  import org.apache.logging.log4j.core.appender.AppenderLoggingException;
34  import org.apache.logging.log4j.core.appender.ManagerFactory;
35  import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager;
36  import org.apache.logging.log4j.core.appender.db.ColumnMapping;
37  import org.apache.logging.log4j.core.config.plugins.convert.DateTypeConverter;
38  import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
39  import org.apache.logging.log4j.core.util.Closer;
40  import org.apache.logging.log4j.spi.ThreadContextMap;
41  import org.apache.logging.log4j.spi.ThreadContextStack;
42  import org.apache.logging.log4j.util.ReadOnlyStringMap;
43  import org.apache.logging.log4j.util.Strings;
44  
45  /**
46   * An {@link AbstractDatabaseManager} implementation for relational databases accessed via JDBC.
47   */
48  public final class JdbcDatabaseManager extends AbstractDatabaseManager {
49  
50      private static final JdbcDatabaseManagerFactory INSTANCE = new JdbcDatabaseManagerFactory();
51  
52      // NOTE: prepared statements are prepared in this order: column mappings, then column configs
53      private final List<ColumnMapping> columnMappings;
54      private final List<ColumnConfig> columnConfigs;
55      private final ConnectionSource connectionSource;
56      private final String sqlStatement;
57  
58      private Connection connection;
59      private PreparedStatement statement;
60      private boolean isBatchSupported;
61  
62      private JdbcDatabaseManager(final String name, final int bufferSize, final ConnectionSource connectionSource,
63                                  final String sqlStatement, final List<ColumnConfig> columnConfigs,
64                                  final List<ColumnMapping> columnMappings) {
65          super(name, bufferSize);
66          this.connectionSource = connectionSource;
67          this.sqlStatement = sqlStatement;
68          this.columnConfigs = columnConfigs;
69          this.columnMappings = columnMappings;
70      }
71  
72      @Override
73      protected void startupInternal() throws Exception {
74          this.connection = this.connectionSource.getConnection();
75          final DatabaseMetaData metaData = this.connection.getMetaData();
76          this.isBatchSupported = metaData.supportsBatchUpdates();
77          Closer.closeSilently(this.connection);
78      }
79  
80      @Override
81      protected boolean shutdownInternal() {
82          if (this.connection != null || this.statement != null) {
83              return this.commitAndClose();
84          }
85          return true;
86      }
87  
88      @Override
89      protected void connectAndStart() {
90          try {
91              this.connection = this.connectionSource.getConnection();
92              this.connection.setAutoCommit(false);
93              this.statement = this.connection.prepareStatement(this.sqlStatement);
94          } catch (final SQLException e) {
95              throw new AppenderLoggingException(
96                      "Cannot write logging event or flush buffer; JDBC manager cannot connect to the database.", e
97              );
98          }
99      }
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 }