You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
404 lines
14 KiB
404 lines
14 KiB
/* |
|
* 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 com.fr.third.apache.log4j; |
|
|
|
import com.fr.third.apache.log4j.helpers.LogLog; |
|
import com.fr.third.apache.log4j.receivers.db.ConnectionSource; |
|
import com.fr.third.apache.log4j.receivers.db.DBHelper; |
|
import com.fr.third.apache.log4j.receivers.db.dialect.SQLDialect; |
|
import com.fr.third.apache.log4j.receivers.db.dialect.Util; |
|
import com.fr.third.apache.log4j.spi.LocationInfo; |
|
import com.fr.third.apache.log4j.spi.LoggingEvent; |
|
import com.fr.third.apache.log4j.xml.DOMConfigurator; |
|
import com.fr.third.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 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; |
|
} |
|
}
|
|
|