blob: e87132e2510ab605712f4fe35558195200b7a19a [file] [log] [blame]
/*
* 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.db;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.db.dialect.SQLDialect;
import org.apache.log4j.db.dialect.Util;
import org.apache.log4j.helpers.LogLog;
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">
* <caption>supported dialects</caption>
* <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&uuml;lc&uuml;
* @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;
}
}