/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.log4j; import org.apache.log4j.helpers.LogLog; import org.apache.log4j.receivers.db.ConnectionSource; import org.apache.log4j.receivers.db.DBHelper; import org.apache.log4j.receivers.db.dialect.SQLDialect; import org.apache.log4j.receivers.db.dialect.Util; import org.apache.log4j.spi.LocationInfo; import org.apache.log4j.spi.LoggingEvent; import org.apache.log4j.xml.DOMConfigurator; import org.apache.log4j.xml.UnrecognizedElementHandler; import org.w3c.dom.Element; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Iterator; import java.util.Properties; import java.util.Set; /** * The DBAppender inserts loggin events into three database tables in a format * independent of the Java programming language. The three tables that * DBAppender inserts to must exists before DBAppender can be used. These tables * may be created with the help of SQL scripts found in the * <em>src/java/org/apache/log4j/db/dialect</em> directory. There is a * specific script for each of the most popular database systems. If the script * for your particular type of database system is missing, it should be quite * easy to write one, taking example on the already existing scripts. If you * send them to us, we will gladly include missing scripts in future releases. * * <p> * If the JDBC driver you are using supports the * {@link java.sql.Statement#getGeneratedKeys}method introduced in JDBC 3.0 * specification, then you are all set. Otherwise, there must be an * {@link SQLDialect}appropriate for your database system. Currently, we have * dialects for PostgreSQL, MySQL, Oracle and MsSQL. As mentioed previously, an * SQLDialect is required only if the JDBC driver for your database system does * not support the {@link java.sql.Statement#getGeneratedKeys getGeneratedKeys} * method. * </p> * * <table border="1" cellpadding="4"> * <tr> * <th>RDBMS</th> * <th>supports <br/><code>getGeneratedKeys()</code> method</th> * <th>specific <br/>SQLDialect support</th> * <tr> * <tr> * <td>PostgreSQL</td> * <td align="center">NO</td> * <td>present and used</td> * <tr> * <tr> * <td>MySQL</td> * <td align="center">YES</td> * <td>present, but not actually needed or used</td> * <tr> * <tr> * <td>Oracle</td> * <td align="center">YES</td> * <td>present, but not actually needed or used</td> * <tr> * <tr> * <td>DB2</td> * <td align="center">YES</td> * <td>not present, and not needed or used</td> * <tr> * <tr> * <td>MsSQL</td> * <td align="center">YES</td> * <td>not present, and not needed or used</td> * <tr> * <tr> * <td>HSQL</td> * <td align="center">NO</td> * <td>present and used</td> * <tr> * * </table> * <p> * <b>Performance: </b> Experiments show that writing a single event into the * database takes approximately 50 milliseconds, on a "standard" PC. If pooled * connections are used, this figure drops to under 10 milliseconds. Note that * most JDBC drivers already ship with connection pooling support. * </p> * * * * <p> * <b>Configuration </b> DBAppender can be configured programmatically, or using * {@link org.apache.log4j.xml.DOMConfigurator JoranConfigurator}. Example * scripts can be found in the <em>tests/input/db</em> directory. * * @author Ceki Gülcü * @author Ray DeCampo */ public class DBAppender extends AppenderSkeleton implements UnrecognizedElementHandler { static final String insertPropertiesSQL = "INSERT INTO logging_event_property (event_id, mapped_key, mapped_value) VALUES (?, ?, ?)"; static final String insertExceptionSQL = "INSERT INTO logging_event_exception (event_id, i, trace_line) VALUES (?, ?, ?)"; static final String insertSQL; private static final Method GET_GENERATED_KEYS_METHOD; static { StringBuffer sql = new StringBuffer(); sql.append("INSERT INTO logging_event ("); sql.append("sequence_number, "); sql.append("timestamp, "); sql.append("rendered_message, "); sql.append("logger_name, "); sql.append("level_string, "); sql.append("ndc, "); sql.append("thread_name, "); sql.append("reference_flag, "); sql.append("caller_filename, "); sql.append("caller_class, "); sql.append("caller_method, "); sql.append("caller_line) "); sql.append(" VALUES (?, ?, ? ,?, ?, ?, ?, ?, ?, ?, ?, ?)"); insertSQL = sql.toString(); // // PreparedStatement.getGeneratedKeys added in JDK 1.4 // Method getGeneratedKeysMethod; try { getGeneratedKeysMethod = PreparedStatement.class.getMethod("getGeneratedKeys", null); } catch(Exception ex) { getGeneratedKeysMethod = null; } GET_GENERATED_KEYS_METHOD = getGeneratedKeysMethod; } ConnectionSource connectionSource; boolean cnxSupportsGetGeneratedKeys = false; boolean cnxSupportsBatchUpdates = false; SQLDialect sqlDialect; boolean locationInfo = false; public DBAppender() { super(false); } public void activateOptions() { LogLog.debug("DBAppender.activateOptions called"); if (connectionSource == null) { throw new IllegalStateException( "DBAppender cannot function without a connection source"); } sqlDialect = Util.getDialectFromCode(connectionSource.getSQLDialectCode()); if (GET_GENERATED_KEYS_METHOD != null) { cnxSupportsGetGeneratedKeys = connectionSource.supportsGetGeneratedKeys(); } else { cnxSupportsGetGeneratedKeys = false; } cnxSupportsBatchUpdates = connectionSource.supportsBatchUpdates(); if (!cnxSupportsGetGeneratedKeys && (sqlDialect == null)) { throw new IllegalStateException( "DBAppender cannot function if the JDBC driver does not support getGeneratedKeys method *and* without a specific SQL dialect"); } // all nice and dandy on the eastern front super.activateOptions(); } /** * @return Returns the connectionSource. */ public ConnectionSource getConnectionSource() { return connectionSource; } /** * @param connectionSource * The connectionSource to set. */ public void setConnectionSource(ConnectionSource connectionSource) { LogLog.debug("setConnectionSource called for DBAppender"); this.connectionSource = connectionSource; } protected void append(LoggingEvent event) { Connection connection = null; try { connection = connectionSource.getConnection(); connection.setAutoCommit(false); PreparedStatement insertStatement; if (cnxSupportsGetGeneratedKeys) { insertStatement = connection.prepareStatement(insertSQL, Statement.RETURN_GENERATED_KEYS); } else { insertStatement = connection.prepareStatement(insertSQL); } /* insertStatement.setLong(1, event.getSequenceNumber());*/ insertStatement.setLong(1, 0); insertStatement.setLong(2, event.getTimeStamp()); insertStatement.setString(3, event.getRenderedMessage()); insertStatement.setString(4, event.getLoggerName()); insertStatement.setString(5, event.getLevel().toString()); insertStatement.setString(6, event.getNDC()); insertStatement.setString(7, event.getThreadName()); insertStatement.setShort(8, DBHelper.computeReferenceMask(event)); LocationInfo li; if (event.locationInformationExists() || locationInfo) { li = event.getLocationInformation(); } else { li = LocationInfo.NA_LOCATION_INFO; } insertStatement.setString(9, li.getFileName()); insertStatement.setString(10, li.getClassName()); insertStatement.setString(11, li.getMethodName()); insertStatement.setString(12, li.getLineNumber()); int updateCount = insertStatement.executeUpdate(); if (updateCount != 1) { LogLog.warn("Failed to insert loggingEvent"); } ResultSet rs = null; Statement idStatement = null; boolean gotGeneratedKeys = false; if (cnxSupportsGetGeneratedKeys) { try { rs = (ResultSet) GET_GENERATED_KEYS_METHOD.invoke(insertStatement, null); gotGeneratedKeys = true; } catch(InvocationTargetException ex) { Throwable target = ex.getTargetException(); if (target instanceof SQLException) { throw (SQLException) target; } throw ex; } catch(IllegalAccessException ex) { LogLog.warn("IllegalAccessException invoking PreparedStatement.getGeneratedKeys", ex); } } if (!gotGeneratedKeys) { insertStatement.close(); insertStatement = null; idStatement = connection.createStatement(); idStatement.setMaxRows(1); rs = idStatement.executeQuery(sqlDialect.getSelectInsertId()); } // A ResultSet cursor is initially positioned before the first row; the // first call to the method next makes the first row the current row rs.next(); int eventId = rs.getInt(1); rs.close(); // we no longer need the insertStatement if(insertStatement != null) { insertStatement.close(); insertStatement = null; } if(idStatement != null) { idStatement.close(); idStatement = null; } Set propertiesKeys = event.getPropertyKeySet(); if (propertiesKeys.size() > 0) { PreparedStatement insertPropertiesStatement = connection.prepareStatement(insertPropertiesSQL); for (Iterator i = propertiesKeys.iterator(); i.hasNext();) { String key = (String) i.next(); String value = event.getProperty(key); //LogLog.info("id " + eventId + ", key " + key + ", value " + value); insertPropertiesStatement.setInt(1, eventId); insertPropertiesStatement.setString(2, key); insertPropertiesStatement.setString(3, value); if (cnxSupportsBatchUpdates) { insertPropertiesStatement.addBatch(); } else { insertPropertiesStatement.execute(); } } if (cnxSupportsBatchUpdates) { insertPropertiesStatement.executeBatch(); } insertPropertiesStatement.close(); insertPropertiesStatement = null; } String[] strRep = event.getThrowableStrRep(); if (strRep != null) { LogLog.debug("Logging an exception"); PreparedStatement insertExceptionStatement = connection.prepareStatement(insertExceptionSQL); for (short i = 0; i < strRep.length; i++) { insertExceptionStatement.setInt(1, eventId); insertExceptionStatement.setShort(2, i); insertExceptionStatement.setString(3, strRep[i]); if (cnxSupportsBatchUpdates) { insertExceptionStatement.addBatch(); } else { insertExceptionStatement.execute(); } } if (cnxSupportsBatchUpdates) { insertExceptionStatement.executeBatch(); } insertExceptionStatement.close(); insertExceptionStatement = null; } connection.commit(); } catch (Throwable sqle) { LogLog.error("problem appending event", sqle); } finally { DBHelper.closeConnection(connection); } } public void close() { closed = true; } /** * Returns value of the <b>LocationInfo </b> property which determines whether * caller's location info is written to the database. */ public boolean getLocationInfo() { return locationInfo; } /** * If true, the information written to the database will include caller's * location information. Due to performance concerns, by default no location * information is written to the database. */ public void setLocationInfo(boolean locationInfo) { this.locationInfo = locationInfo; } /** * Gets whether appender requires a layout. * @return false */ public boolean requiresLayout() { return false; } /** * {@inheritDoc} */ public boolean parseUnrecognizedElement(Element element, Properties props) throws Exception { if ("connectionSource".equals(element.getNodeName())) { Object instance = DOMConfigurator.parseElement(element, props, ConnectionSource.class); if (instance instanceof ConnectionSource) { ConnectionSource source = (ConnectionSource) instance; source.activateOptions(); setConnectionSource(source); } return true; } return false; } }