package de.is24.util.monitoring.database;
import de.is24.util.monitoring.InApplicationMonitor;
import de.is24.util.monitoring.StateValueProvider;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Savepoint;
import java.sql.Statement;
import java.sql.Struct;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
/**
* <p>
* The <code>MonitoringDataSource</code> wraps a given <code>DataSource</code>
* and the subsequently returned <code>Connections</code>. The combination of
* <code>MonitoringDataSource</code> and {@link MonitoringConnection} monitor
* </p>
* the number of requested database connections split into
* <ul>
* <li>unpersonalised ({@link #getConnection()}) and</li>
* <li>personalised connections {@link #getConnection(String, String)}</li>
* <li>the maximum number of connections used in parallel</li>
* <li>the number of errors thrown when calling</li>
* </ul>
* {@link java.sql.Connection#commit()}
* {@link java.sql.Connection#rollback()}
* {@link java.sql.Connection#rollback(java.sql.Savepoint)}
* <p>
* Additionally, the returned {@link java.sql.Statement}s are wrapped and attempt to log
* the SQL being executed in case an exception is thrown.
* </p>
*
* If you want to use this Class, you need to require springframework core.
*
* @author Sebastian Kirsch
* @see #getConnection()
* @see #getConnection(String, String)
*/
public class MonitoringDataSource implements DataSource {
private static final Logger LOGGER = LoggerFactory.getLogger(MonitoringDataSource.class);
/** The wrapped <code>DataSource</code>. */
private final DataSource original;
/** The base name for all monitor values. */
private final String monitorBaseName;
/** This field is used to trace the maximum number of connections being use in parallel. */
private final AtomicInteger maxConnections;
/** This field is a helper to determine the correct for {@link #maxConnections}. */
private final AtomicInteger currentConnections;
/** These predicates indicate if a the executed SQL should be logged at INFO level. */
final Set<Predicate<SQLException>> loggingFilters = new HashSet<Predicate<SQLException>>();
// CONSTRUCTORS //////////////////////////////////////////////////////////////
/**
* Creates a new instance of <code>MonitoringDataSource</code>.
*
* @param dataSource
* the <code>DataSource</code> to wrap
* @param monitorBaseName
* the monitor name to insert such that "de.is24.common.database.MonitoringDataSource.<i>monitorBaseName</i>.monitoringKey" is reported
* @throws IllegalArgumentException
* if <code>dataSource</code> is <code>null</code>
*/
public MonitoringDataSource(DataSource dataSource, String monitorBaseName) {
if (dataSource == null) {
throw new IllegalArgumentException("DataSource is null");
}
this.original = dataSource;
this.monitorBaseName = MonitoringDataSource.class.getName() +
(StringUtils.isBlank(monitorBaseName) ? "" : ("." + monitorBaseName));
this.currentConnections = new AtomicInteger(0);
this.maxConnections = new AtomicInteger(0);
InApplicationMonitor.getInstance().registerStateValue(new StateValueProvider() {
@Override
public String getName() {
return MonitoringDataSource.this.monitorBaseName + ".maxOpenConnections";
}
@Override
public long getValue() {
return MonitoringDataSource.this.maxConnections.get();
}
});
// initialise InApplicationMonitor
InApplicationMonitor.getInstance()
.initializeCounter(MonitoringDataSource.this.monitorBaseName +
".error.getConnection");
InApplicationMonitor.getInstance().initializeCounter(MonitoringDataSource.this.monitorBaseName +
".error.commit");
InApplicationMonitor.getInstance()
.initializeCounter(MonitoringDataSource.this.monitorBaseName +
".getPersonalisedConnection");
InApplicationMonitor.getInstance().initializeCounter(
MonitoringDataSource.this.monitorBaseName + ".error.rollback");
InApplicationMonitor.getInstance()
.initializeCounter(
MonitoringDataSource.this.monitorBaseName + ".error.rollbackSavepoint");
}
/**
* Creates a new instance of <code>MonitoringDataSource</code>
* without a specific monitoring base name.
*
* @param dataSource
* the <code>DataSource</code> to wrap
* @throws IllegalArgumentException
* if <code>dataSource</code> is <code>null</code>
*/
public MonitoringDataSource(DataSource dataSource) {
this(dataSource, null);
}
// METHODS ///////////////////////////////////////////////////////////////////
public void addExceptionLogFilter(Predicate<SQLException> predicate) {
this.loggingFilters.add(predicate);
}
public void setExceptionLogFilters(String configuration) {
this.loggingFilters.clear();
if (StringUtils.isEmpty(configuration)) {
return;
}
configureSqlExceptionPredicates(configuration);
}
private void configureSqlExceptionPredicates(String configuration) {
for (String configEntry : configuration.split("[,]")) {
String[] configEntryItems = configEntry.split("[:]");
if (configEntryItems.length > 2) {
throw new IllegalArgumentException(
"The config entry [" + configEntry +
"] contains more than one ':' and thus is invalid!");
}
int errorCode = Integer.parseInt(configEntryItems[0]);
String regex = (configEntryItems.length > 1) ? configEntryItems[1] : null;
this.loggingFilters.add(new SqlExceptionPredicate(errorCode, regex));
}
}
/**
* Handles the monitoring for retrieving connections and adapts the <i>max connection</i> counter if appropriate.
*
* @param startingInstant
* the instant a database connection was requested
* @param monitorSuffix
* the suffix for the monitor name to increase
*/
private void doConnectionMonitoring(long startingInstant, String monitorSuffix) {
InApplicationMonitor.getInstance()
.addTimerMeasurement(this.monitorBaseName + monitorSuffix,
System.currentTimeMillis() - startingInstant);
int noCurrentConnections = this.currentConnections.incrementAndGet();
if (noCurrentConnections > this.maxConnections.get()) {
this.maxConnections.set(noCurrentConnections);
}
}
/** Increases the error count for the getConnection method. */
private void monitorFailedConnectionAttempt() {
InApplicationMonitor.getInstance()
.incrementCounter(MonitoringDataSource.this.monitorBaseName +
".error.getConnection");
}
/**
* Executes the specified {@link java.util.concurrent.Callable} to fetch a connection.
* Monitors occurring exceptions/errors.
*
* @param callable
* the <code>Callable</code> that actually fetches a <code>Connection</code>
* @param monitorSuffix
* the suffix for the monitor name to increase (forwarded to {@link #doConnectionMonitoring(long, String)})
* @return a database <code>Connection</code>
* @throws java.sql.SQLException
* if fetching a <code>Connection</code> fails
* @throws RuntimeException
*/
private Connection getConnection(Callable<Connection> callable, String monitorSuffix) throws SQLException {
final long now = System.currentTimeMillis();
try {
MonitoringConnection c = new MonitoringConnection(callable.call());
doConnectionMonitoring(now, monitorSuffix);
return c;
// CSOFF: IllegalCatch
// CSOFF: IllegalThrows
} catch (Error e) {
monitorFailedConnectionAttempt();
throw e;
} catch (RuntimeException rE) {
monitorFailedConnectionAttempt();
// Well, this MAY happen, although it shouldn't
throw rE;
// CSON: IllegalCatch
// CSON: IllegalThrows
} catch (SQLException sqlE) {
monitorFailedConnectionAttempt();
// sad but true
throw sqlE;
} catch (Exception e) {
monitorFailedConnectionAttempt();
// This MUST NOT happen - meaning that someone frakked the code of this class
LOGGER.error(
"Unexpected Exception thrown by Callable; please check source code of de.is24.common.database.MonitoringDataSource: " +
e);
throw new RuntimeException(e);
}
}
// java.lang.Object //////////////////////////////////////////////////////////
/**
* Returns a String representation of this object.
*
* @return a <code>String</code> representing this instance
*/
@Override
public String toString() {
return this.monitorBaseName.substring(MonitoringDataSource.class.getName().lastIndexOf('.') + 1) + " wrapping [" +
this.original + "]";
}
// javax.sql.DataSource //////////////////////////////////////////////////////
/**
* <p>
* Delegates the call to the wrapped data source, wraps the returned
* connection and counts the connections returned.
* </p>
*
* @return a database <code>Connection</code>
* @throws java.sql.SQLException
* as a result of the delegation
*/
@Override
public Connection getConnection() throws SQLException {
return getConnection(
new Callable<Connection>() {
@Override
public Connection call() throws SQLException {
return MonitoringDataSource.this.original.getConnection();
}
},
".getConnection");
}
/**
* <p>
* Delegates the call to the wrapped data source, wraps the returned
* connection and counts the connections returned.
* </p>
*
* @param username
* the name of the database user on whose behalf the connection isbeing made
* @param password
* the user's password
* @return a database <code>Connection</code>
* @throws java.sql.SQLException
* as a result of the delegation
*/
@Override
public Connection getConnection(final String username, final String password) throws SQLException {
return getConnection(
new Callable<Connection>() {
@Override
public Connection call() throws SQLException {
return MonitoringDataSource.this.original.getConnection(username, password);
}
},
".getPersonalisedConnection");
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return this.original.getLogWriter();
}
@Override
public int getLoginTimeout() throws SQLException {
return this.original.getLoginTimeout();
}
@Override
public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
return this.original.getParentLogger();
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
this.original.setLogWriter(out);
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
this.original.setLoginTimeout(seconds);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return original.isWrapperFor(iface);
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return original.unwrap(iface);
}
/**
* <p>The <code>MonitoringConnection</code> is integral part of the {@link MonitoringDataSource}.</p>
*
* @author <a href="mailto:SKirsch@is24.de">Sebastian Kirsch</a>
* @see MonitoringDataSource
* @see #close()
* @see #commit()
* @see #rollback()
* @see #rollback(java.sql.Savepoint)
*/
private class MonitoringConnection implements Connection {
private final Connection original;
private final long creationTime = System.currentTimeMillis();
public MonitoringConnection(Connection connection) {
if (connection == null) {
throw new NullPointerException("The Connection cannot be null!");
}
this.original = connection;
}
private Object wrapStatementWithSqlLoggingProxy(Statement statement, String sql) {
Class<? extends Statement> clazz = statement.getClass();
Object proxiedStatement = Proxy.newProxyInstance(clazz.getClassLoader(),
clazz.getInterfaces(),
new SqlLoggingInvocationHandler(MonitoringDataSource.this.loggingFilters, statement, sql));
return proxiedStatement;
}
private Object wrapStatementWithSqlLoggingProxy(Statement statement) {
return wrapStatementWithSqlLoggingProxy(statement, null);
}
// java.lang.Object ////////////////////////////////////////////////////////
/**
* Returns a String representation of this object.
*
* @return a <code>String</code> representing this instance
*/
@Override
public String toString() {
return "MonitoringConnection from " +
MonitoringDataSource.this.monitorBaseName.substring(MonitoringDataSource.class.getName().lastIndexOf('.') + 1) +
" wrapping [" + this.original + "]";
}
// java.sql.Connection ///////////////////////////////////////////////////
/**
* <p>Delegates the call to the wrapped data source, counting the number of calls made.</p>
*
* @throws java.sql.SQLException
* as a result of the delegation
*/
@Override
public void close() throws SQLException {
try {
this.original.close();
} finally {
MonitoringDataSource.this.currentConnections.decrementAndGet();
InApplicationMonitor.getInstance().incrementCounter(MonitoringDataSource.this.monitorBaseName + ".close");
InApplicationMonitor.getInstance()
.addTimerMeasurement(MonitoringDataSource.this.monitorBaseName + ".usage",
this.creationTime,
System.currentTimeMillis());
}
}
/**
* <p>Delegates the call to the wrapped data source, counting the number of exceptions thrown.</p>
*
* @throws java.sql.SQLException
* as a result of the delegation
*/
@Override
public void commit() throws SQLException {
try {
this.original.commit();
} catch (SQLException sqlE) {
InApplicationMonitor.getInstance()
.incrementCounter(MonitoringDataSource.this.monitorBaseName +
".error.commit");
throw sqlE;
}
}
/**
* <p>Delegates the call to the wrapped data source, counting the number of exceptions thrown.</p>
*
* @throws java.sql.SQLException
* as a result of the delegation
*/
@Override
public void rollback() throws SQLException {
try {
this.original.rollback();
} catch (SQLException sqlE) {
InApplicationMonitor.getInstance()
.incrementCounter(
MonitoringDataSource.this.monitorBaseName + ".error.rollback");
throw sqlE;
}
}
/**
* <p>Delegates the call to the wrapped data source, counting the number of exceptions thrown.</p>
*
* @throws java.sql.SQLException
* as a result of the delegation
*/
@Override
public void rollback(Savepoint savepoint) throws SQLException {
try {
this.original.rollback(savepoint);
} catch (SQLException sqlE) {
InApplicationMonitor.getInstance()
.incrementCounter(
MonitoringDataSource.this.monitorBaseName + ".error.rollbackSavepoint");
throw sqlE;
}
}
@Override
public void clearWarnings() throws SQLException {
this.original.clearWarnings();
}
@Override
public Statement createStatement() throws SQLException {
return (Statement) wrapStatementWithSqlLoggingProxy(this.original.createStatement());
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability)
throws SQLException {
return (Statement) wrapStatementWithSqlLoggingProxy(
this.original.createStatement(resultSetType,
resultSetConcurrency, resultSetHoldability));
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
return (Statement) wrapStatementWithSqlLoggingProxy(
this.original.createStatement(resultSetType,
resultSetConcurrency));
}
@Override
public boolean getAutoCommit() throws SQLException {
return this.original.getAutoCommit();
}
@Override
public String getCatalog() throws SQLException {
return this.original.getCatalog();
}
@Override
public int getHoldability() throws SQLException {
return this.original.getHoldability();
}
@Override
public DatabaseMetaData getMetaData() throws SQLException {
return this.original.getMetaData();
}
@Override
public int getTransactionIsolation() throws SQLException {
return this.original.getTransactionIsolation();
}
@Override
public Map<String, Class<?>> getTypeMap() throws SQLException {
return this.original.getTypeMap();
}
@Override
public SQLWarning getWarnings() throws SQLException {
return this.original.getWarnings();
}
@Override
public boolean isClosed() throws SQLException {
return this.original.isClosed();
}
@Override
public boolean isReadOnly() throws SQLException {
return this.original.isReadOnly();
}
@Override
public String nativeSQL(String sql) throws SQLException {
return this.original.nativeSQL(sql);
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency,
int resultSetHoldability) throws SQLException {
return (CallableStatement) wrapStatementWithSqlLoggingProxy(
this.original.prepareCall(sql, resultSetType,
resultSetConcurrency, resultSetHoldability),
sql);
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
return (CallableStatement) wrapStatementWithSqlLoggingProxy(
this.original.prepareCall(sql, resultSetType,
resultSetConcurrency),
sql);
}
@Override
public CallableStatement prepareCall(String sql) throws SQLException {
return (CallableStatement) wrapStatementWithSqlLoggingProxy(this.original.prepareCall(sql), sql);
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency,
int resultSetHoldability) throws SQLException {
return (PreparedStatement) wrapStatementWithSqlLoggingProxy(
this.original.prepareStatement(sql, resultSetType,
resultSetConcurrency, resultSetHoldability),
sql);
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
throws SQLException {
return (PreparedStatement) wrapStatementWithSqlLoggingProxy(
this.original.prepareStatement(sql, resultSetType,
resultSetConcurrency),
sql);
}
@Override
public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
return (PreparedStatement) wrapStatementWithSqlLoggingProxy(
this.original.prepareStatement(sql,
autoGeneratedKeys),
sql);
}
@Override
public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
return (PreparedStatement) wrapStatementWithSqlLoggingProxy(this.original.prepareStatement(sql, columnIndexes),
sql);
}
@Override
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
return (PreparedStatement) wrapStatementWithSqlLoggingProxy(this.original.prepareStatement(sql, columnNames),
sql);
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return (PreparedStatement) wrapStatementWithSqlLoggingProxy(this.original.prepareStatement(sql), sql);
}
@Override
public void releaseSavepoint(Savepoint savepoint) throws SQLException {
this.original.releaseSavepoint(savepoint);
}
@Override
public void setAutoCommit(boolean autoCommit) throws SQLException {
this.original.setAutoCommit(autoCommit);
}
@Override
public void setCatalog(String catalog) throws SQLException {
this.original.setCatalog(catalog);
}
@Override
public void setHoldability(int holdability) throws SQLException {
this.original.setHoldability(holdability);
}
@Override
public void setReadOnly(boolean readOnly) throws SQLException {
this.original.setReadOnly(readOnly);
}
@Override
public Savepoint setSavepoint() throws SQLException {
return this.original.setSavepoint();
}
@Override
public Savepoint setSavepoint(String name) throws SQLException {
return this.original.setSavepoint(name);
}
@Override
public void setTransactionIsolation(int level) throws SQLException {
this.original.setTransactionIsolation(level);
}
@Override
public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
this.original.setTypeMap(map);
}
@Override
public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
return original.createArrayOf(typeName, elements);
}
@Override
public Blob createBlob() throws SQLException {
return original.createBlob();
}
@Override
public Clob createClob() throws SQLException {
return original.createClob();
}
@Override
public NClob createNClob() throws SQLException {
return original.createNClob();
}
@Override
public SQLXML createSQLXML() throws SQLException {
return original.createSQLXML();
}
@Override
public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
return original.createStruct(typeName, attributes);
}
@Override
public void setSchema(String schema) throws SQLException {
this.original.setSchema(schema);
}
@Override
public String getSchema() throws SQLException {
return this.original.getSchema();
}
@Override
public void abort(Executor executor) throws SQLException {
this.original.abort(executor);
}
@Override
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
this.original.setNetworkTimeout(executor, milliseconds);
}
@Override
public int getNetworkTimeout() throws SQLException {
return this.original.getNetworkTimeout();
}
@Override
public Properties getClientInfo() throws SQLException {
return original.getClientInfo();
}
@Override
public String getClientInfo(String name) throws SQLException {
return original.getClientInfo(name);
}
@Override
public boolean isValid(int timeout) throws SQLException {
return original.isValid(timeout);
}
@Override
public void setClientInfo(Properties properties) throws SQLClientInfoException {
original.setClientInfo(properties);
}
@Override
public void setClientInfo(String name, String value) throws SQLClientInfoException {
original.setClientInfo(name, value);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return original.isWrapperFor(iface);
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return original.unwrap(iface);
}
}
private static class SqlLoggingInvocationHandler implements InvocationHandler {
private final Object wrappedObject;
private final String preparedSql;
final Set<Predicate<SQLException>> loggingFilters;
public SqlLoggingInvocationHandler(Set<Predicate<SQLException>> loggingFilters, Object wrappedObject,
String preparedSql) {
this.loggingFilters = loggingFilters;
this.wrappedObject = wrappedObject;
this.preparedSql = preparedSql;
}
@Override
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
try {
return method.invoke(this.wrappedObject, arguments);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot execute wrapped call [" + method.getName() + "()@" + proxy + "]", e);
} catch (InvocationTargetException iTE) {
logExecutedSqlIfApplicable(method, arguments, iTE);
throw iTE.getTargetException();
}
}
private void logExecutedSqlIfApplicable(Method method, Object[] arguments, InvocationTargetException iTE) {
if (!anExecuteMethodIsCalled(method)) {
return;
}
String sql = determineExecutedSql(arguments);
if (sql == null) {
return;
}
logExecutedSqlAtTheAppropriateLevel(iTE, sql);
}
private boolean anExecuteMethodIsCalled(Method method) {
return method.getName().startsWith("execute");
}
private String determineExecutedSql(Object[] arguments) {
String sql = this.preparedSql;
if ((arguments != null) && (arguments[0] instanceof String)) {
sql = (String) arguments[0];
}
return sql;
}
private void logExecutedSqlAtTheAppropriateLevel(InvocationTargetException iTE, String sql) {
if (shouldLogAsWarn(iTE)) {
LOGGER.warn("Failed to execute [{}]: {}", sql, iTE.getTargetException());
} else {
LOGGER.info("Failed to execute [[{}]: {}", sql, iTE.getTargetException());
}
}
private boolean shouldLogAsWarn(InvocationTargetException iTE) {
boolean logAsWarn = true;
int index = ExceptionUtils.indexOfType(iTE, SQLException.class);
if (index >= 0) {
SQLException sqlException = (SQLException) ExceptionUtils.getThrowables(iTE)[index];
for (Predicate<SQLException> predicate : this.loggingFilters) {
if (predicate.apply(sqlException)) {
logAsWarn = false;
break;
}
}
}
return logAsWarn;
}
}
static class SqlExceptionPredicate implements Predicate<SQLException> {
private final int errorCode;
private final String messagePattern;
public SqlExceptionPredicate(int errorCode, String messagePattern) {
this.errorCode = errorCode;
this.messagePattern = messagePattern;
}
@Override
public boolean apply(SQLException input) {
if (this.errorCode != input.getErrorCode()) {
return false;
}
if (this.messagePattern == null) {
return true;
}
return (input.getMessage() != null) && input.getMessage().matches(this.messagePattern);
}
}
protected interface Predicate<T> {
boolean apply(T input);
@Override
boolean equals(Object object);
}
}