1. About

1.1. What is datasource-proxy?

datasource-proxy provides a simple API for intercepting JDBC interactions and lets you add your own logic before or after query execution or method calls.
The proxy works as a javax.sql.DataSource and takes listeners. Predefined listeners support query logging, slow query detection, query execution statistics, interaction tracing, and more. You can also easily add custom listeners to the listener chain.

1.2. Built-in Support

Query and Parameter Logging

You can log executed database queries and parameters using your preferred logging framework (Commons Logging, SLF4J, Java Util Logging, System.out, or your own implementation). The output can be formatted as JSON.

Slow Query Detection and Logging

You can register callbacks for queries that exceed a configured threshold. Slow query loggers are available out of the box.

Connection ID

Each connection is assigned a unique ID, implemented as a sequentially increasing number by default. The connection ID is included in the log entry.

Query Metrics

You can collect statistics for executed queries, such as total time and the number of SELECT, INSERT, UPDATE, and DELETE statements. The output can be formatted as JSON.

Custom Logic Injection for Query and Method Execution

You can write your own QueryExecutionListener implementations and have them invoked before or after query execution. MethodExecutionListener is also invoked for each JDBC API interaction.

Web Application Support

ProxyDataSource can be configured via JNDI. Query metrics are available on a per-web-request basis (request-response lifecycle). For JSP-based UIs, a custom tag for accessing metrics (<dsp:metrics/>) is also available.

Query and Parameter Replacement

QueryTransformer and ParameterTransformer allow you to modify the executing query and parameters just before they are sent to the database.

Method Tracing

Intercept JDBC API interactions such as obtaining a connection, creating prepared statements, setting parameters, and executing queries.

1.3. Jakarta Namespace

The javax.sql.* classes are part of the Java SE APIs, so they are not affected by the namespace change in Java EE APIs to Jakarta. For example, the javax.sql.DataSource class remains in the javax.sql package.

In datasource-proxy, there are a few usages of the javax namespace in JNDI and query count support via ServletRequestListener. These are not used frequently and are expected to be removed or separated in the next major version release.

If you require these features in the Jakarta namespace, it is recommended that you create your own classes based on the existing implementations.

1.4. Sample Log Output

Query execution (single line):

Name:MyProxy, Connection:1, Time:1, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["CREATE TABLE users(id INT, name VARCHAR(255))"], Params:[]
Name:MyProxy, Connection:2, Time:5, Success:True, Type:Prepared, Batch:True, QuerySize:1, BatchSize:2, Query:["INSERT INTO users (id, name) VALUES (?, ?)"], Params:[(1,foo),(2,bar)]

Query execution (multiple lines):

Name:MyProxy, Connection:1, Time:3, Success:True
Type:Callable, Batch:True, QuerySize:1, BatchSize:2
Query:["{call getEmployer (?, ?)}"]
Params:[(id=100,name=foo),(id=200,name=bar)]

JSON output:

{"name":"MyProxy", "connection":1, "time":1, "success":true, "type":"Statement", "batch":false, "querySize":1, "batchSize":0, "query":["CREATE TABLE users(id INT, name VARCHAR(255))"], "params":[]}
{"name":"MyProxy", "connection":2, "time":0, "success":true, "type":"Prepared", "batch":true, "querySize":1, "batchSize":3, "query":["INSERT INTO users (id, name) VALUES (?, ?)"], "params":[["1","foo"],["2","bar"],["3","baz"]]}

Query metrics:

Name:"MyProxy", Time:6, Total:1, Success:1, Failure:0, Select:1, Insert:0, Update:0, Delete:0, Other:0
// JSON output
{"name":"MyProxy", "time":10, "total":3, "success":3, "failure":0, "select":1, "insert":2, "update":0, "delete":0, "other":0}

Method tracing:

Output:
[1][success][0ms][conn=1] ProxyDataSource#getConnection()
[2][success][1ms][conn=1] JDBCConnection#prepareStatement("INSERT INTO users (id, name) VALUES (?, ?)")
[3][success][0ms][conn=1] JDBCPreparedStatement#setString(2,"FOO")
[4][success][0ms][conn=1] JDBCPreparedStatement#setInt(1,100)
[5][success][0ms][conn=1] JDBCPreparedStatement#addBatch()
[6][success][0ms][conn=1] JDBCPreparedStatement#setInt(1,200)
[7][success][0ms][conn=1] JDBCPreparedStatement#setString(2,"BAR")
[8][success][0ms][conn=1] JDBCPreparedStatement#addBatch()
[9][success][1ms][conn=1] JDBCPreparedStatement#executeBatch()
[10][success][0ms][conn=1] JDBCPreparedStatement#close()
[11][success][0ms][conn=1] JDBCConnection#close()

2. Installation

2.1. Dependency

<dependency>
  <groupId>net.ttddyy</groupId>
  <artifactId>datasource-proxy</artifactId>
  <version>[LATEST_VERSION]</version>
</dependency>

There is no required dependency for using datasource-proxy.
If you choose to use logging support for a specific logging library, such as Commons Logging or SLF4J, you need to add that dependency explicitly.

datasource-proxy does not bring in any libraries via transitive dependencies.

2.1.1. Snapshot

Snapshots are available from the OSS Sonatype snapshot repository.

To download snapshot JARs, enable the Sonatype snapshot repository:

<repositories>
  <repository>
    <id>sonatype-snapshots-repo</id>
    <url>https://oss.sonatype.org/content/repositories/snapshots</url>
    <releases>
      <enabled>false</enabled>
    </releases>
    <snapshots>
      <enabled>true</enabled>
    </snapshots>
  </repository>
</repositories>

2.2. Supported Java Versions

datasource-proxy works with Java 6 or later.
Java 8 (JDBC 4.2) works well, and support for Java 9 (JDBC 4.3) specific features is planned.

3. How to use

ProxyDataSource implements javax.sql.DataSource and serves as the entry point for all interceptor logic. You can directly instantiate ProxyDataSource, or use the ProxyDataSourceBuilder builder class to create it.
Once ProxyDataSource is set up, you can pass it to your application as a datasource.

3.1. Creating ProxyDataSource

ProxyDataSourceBuilder provides a fluent API for constructing a ProxyDataSource instance. It also provides builder methods for registering built-in and custom listeners.

DataSource dataSource =
    ProxyDataSourceBuilder.create(actualDataSource)  // pass the original datasource
        .logQueryByCommons(INFO)    // logQueryBySlf4j(), logQueryByJUL(), logQueryToSysOut()
        .loggingFilter(myFilter)    // filter which queries to log
        .countQuery()               // enable query count metrics
        .logSlowQueryByCommons(10, TimeUnit.MINUTES)  // also by slf4j, jul, system out
        .proxyResultSet()           // enable proxying ResultSet
        .listener(myListener)       // register a custom listener
        .afterMethod(executionContext -> {    // register a custom listener with lambda
            ...
        })
        .build();  // returns a ProxyDataSource instance

3.1.1. Creating a proxied DataSource

Starting from datasource-proxy v1.10, the new buildProxy() method returns a pure DataSource proxy instance.

The difference between the ProxyDataSource returned by build() and the DataSource proxy returned by buildProxy() is as follows:

  • ProxyDataSource is a concrete implementation class. This class provides method callbacks only for the getConnection methods.

  • The DataSource proxy is a pure proxy instance. It provides method callbacks for all methods.

DataSource dataSource =
    ProxyDataSourceBuilder.create(actualDataSource)  // pass the original datasource
        ...
        .buildProxy();  // returns DataSource

3.1.2. Retrieving the original JDBC object

All proxied JDBC objects implement the ProxyJdbcObject interface, which defines getTarget() to retrieve the original JDBC object.

if (connection instanceof ProxyJdbcObject) {
    Connection original = (Connection) ((ProxyJdbcObject) connection).getTarget();
    ....
}

3.1.3. DriverManager and Connection Support

From an instance of java.sql.Connection, you can create a proxy that behaves the same way as the one returned by ProxyDataSource.

// obtain a connection
Class.forName("org.hsqldb.jdbcDriver");
Connection realConnection = DriverManager.getConnection("jdbc:hsqldb:mem:aname");

ProxyConfig proxyConfig = ProxyConfig.Builder.create()
                             // configure more
                            .build();

JdbcProxyFactory jdbcProxyFactory = new JdkJdbcProxyFactory();
Connection proxyConnection = jdbcProxyFactory.createConnection(realConnection, proxyConfig);
...

3.1.4. Spring XML Configuration Support

Because ProxyConfig is created using a builder-style API, it is not straightforward to create its bean in an XML-based Spring configuration. ProxyConfigSpringXmlSupport provides setters to build a ProxyConfig specifically for use in XML-based Spring configuration files.

Sample XML config
<bean id="dataSource" primary="true" class="net.ttddyy.dsproxy.support.ProxyDataSource">
  <property name="dataSource" ref="actualDataSource"/>
  <property name="proxyConfig" ref="proxyConfig"/>
  <!--
    Alternatively, you can use SpEL to invoke the create() method to get the ProxyConfig bean.

    <property name="proxyConfig" value="#{proxyConfigSupport.create()}"/>
    -->
</bean>

<bean id="proxyConfig"
      factory-bean="proxyConfigSupport"
      factory-method="create"/>

<bean id="proxyConfigSupport" class="net.ttddyy.dsproxy.support.ProxyConfigSpringXmlSupport">
  <property name="dataSourceName" value="my-ds"/>
  <property name="queryListener" ref="queryListener"/>
  <property name="methodListener" ref="methodListener"/>
</bean>

<bean id="queryListener" class="net.ttddyy.dsproxy.listener.ChainListener">
  <property name="listeners">
    <list>
      <bean class="net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener"/>
    </list>
  </property>
</bean>

<bean id="methodListener" class="net.ttddyy.dsproxy.listener.CompositeMethodListener">
  <property name="listeners">
    <list>
      <bean class="net.ttddyy.dsproxy.listener.TracingMethodListener"/>
    </list>
  </property>
</bean>

3.1.5. JNDI Support

<Resource name="jdbc/global/myProxy"
          auth="Container"
          type="net.ttddyy.dsproxy.support.ProxyDataSource"
          factory="net.ttddyy.dsproxy.support.jndi.ProxyDataSourceObjectFactory"
          description="ds"
          listeners="commons,count"
          proxyName="MyProxy"
          dataSource="[REFERENCE_TO_ACTUAL_DATASOURCE_RESOURCE]"  <!-- ex: java:jdbc/global/myDS -->
/>

The datasource-proxy JAR and your chosen logging library (Commons, SLF4J, etc.) must be accessible from the container.

Table 1. JNDI Resource parameters
Parameter Description

dataSource (required)

Reference to the actual datasource resource, for example: java:jdbc/global/myDS

proxyName

ProxyDataSource name

logLevel

Log level for Commons Logging or SLF4J, for example: DEBUG, INFO, etc.

loggerName

Logger name (since v1.3.1)

listeners

Fully qualified class name of a QueryExecutionListener implementation, or one of the predefined values below. Can be comma-delimited.

queryTransformer

Fully qualified class name of a QueryTransformer implementation.

parameterTransformer

Fully qualified class name of a ParameterTransformer implementation.

Table 2. JNDI Resource "listeners" parameter
Parameter Description

sysout

Alias to net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener

commons

Alias to net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener

slf4j

Alias to net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener

count

Alias to net.ttddyy.dsproxy.listener.logging.DataSourceQueryCountListener

x.y.z.MyQueryExecutionListener

Fully qualified class name of a QueryExecutionListener implementation

Table 3. JNDI Resource "format" parameter
Parameter Description

json

Set the logging output format to JSON

See Tomcat configuration examples in the datasource-proxy-examples project:

Also see the ProxyDataSourceObjectFactory Javadoc.

3.2. Generated Keys

3.2.1. Auto-retrieve generated keys

When 1) generated keys are available (for example, Statement#RETURN_GENERATED_KEYS is specified when a statement is executed or when a prepared/callable statement is created) and 2) query execution methods (execute, executeUpdate, executeBatch, etc.) are called, datasource-proxy automatically calls getGeneratedKeys() and makes the returned ResultSet available via ExecutionInfo#getGeneratedKeys() in QueryExecutionListener.

To enable auto-retrieval of generated keys:

builder
  .autoRetrieveGeneratedKeys(...)
  .build();
When getGeneratedKeys() is called:
If a cached auto-retrieved generated key result set is still open, it is returned. If there is no cached generated key result set, or the cached one has already been closed, datasource-proxy performs the actual getGeneratedKeys() call on the underlying Statement object, caches the result, and then returns it.

If you want to always retrieve generated keys without changing application code that may be executing a query without Statement#RETURN_GENERATED_KEYS, see Replace the invoking method and parameters. You can modify query executions to set RETURN_GENERATED_KEYS.

For batch execution: executeBatch() and executeLargeBatch()

According to the JDBC specification, returning getGeneratedKeys() for executeBatch() and executeLargeBatch() is implementation-specific:

Note – It is implementation-defined as to whether Statement.getGeneratedKeys will return generated values after invoking the executeBatch or executeLargeBatch methods.

— from JDBC specification

In datasource-proxy, whether to auto-retrieve generated keys for batch execution is configurable through ProxyDataSourceBuilder#retrieveGeneratedKeysForBatch().

builder
  .autoRetrieveGeneratedKeys(...)
  .retrieveGeneratedKeysForBatch(false, true)  // first argument is for Statement,
                                               // second is for Prepared/Callable
  .build();

Default behavior for batch executions:

Table 4. Default behavior for auto retrieval of generated keys during batch execution
Type Auto Retrieval

Statement

Not retrieved

Prepared/Callable

Retrieved

3.2.2. Auto-close generated keys

When auto-close is enabled, the auto-retrieved generated keys are closed after all QueryExecutionListener#afterQuery() callbacks have been processed.

This option is used with auto retrieve generated keys feature.

builder
  .autoRetrieveGeneratedKeys(true)  // specify `true` or `false` to auto-close the `ResultSet`
  .build();

If you want to reuse the auto-retrieved generated-keys in upper layer, such as OR mapper or application, turn off this auto-close feature; so that, cached generated-keys will be returned when getGeneratedKeys() is called. Based on jdbc-driver implementation, calling getGeneratedKeys() might be limited to once. When you decided to reuse generated-keys, you might need to support repeatable read on the ResultSet. see Proxying generated keys for how to support repeatable read on generated-keys.

When getGeneratedKeys() is called, the returned ResultSet will NOT be closed by this feature since it is not auto-retrieved generated-keys.

3.2.3. Proxying generated keys

Create a proxy for generated-keys' ResultSet. This applies to both auto-retrieved generated-keys and result of getGeneratedKeys().

Usecase

You have a QueryExecutionListener that reads values from generated-keys' ResultSet. Also, in upper layer, such as ORM or application logic, it needs to use the same generated-keys' ResultSet.

Based on ResultSet implementation, reading values may be limited to only once; therefore, when QueryExecutionListener reads values from generated-keys' ResultSet and upper layer tries to read values from it, the ResultSet may not allow the repeated read.

To allow multiple reads, you can proxy the generated-keys' ResultSet with RepeatableReadResultSetProxyLogic.

builder
   // specify proxy factory for generated-keys' result-set
  .autoRetrieveGeneratedKeys(false, new RepeatableReadResultSetProxyLogicFactory());
  // equivalent of above
  //   .autoRetrieveGeneratedKeysWithRepeatableReadProxy(false);
  .build();

3.3. Proxying ResultSet

By default, datasource-proxy does NOT proxy ResultSet. However, in some case, you want to return a proxied ResultSet - for example, apply MethodExecutionListener on ResultSet.

To enable ResultSet proxy, ProxyDataSourceBuilder has proxyResultSet() method.

builder
  // configure listeners
  .proxyResultSet()  // enable proxying result set
  .build();

In addition, the proxyResultSet() method takes ResultSetProxyLogicFactory to add some logic on interacting with ResultSet. Default logic is no-op.

This proxying does NOT apply to the ResultSet from getGeneratedKeys(). To proxy the result of getGeneratedKeys(), see Proxying generated keys.

3.3.1. Repeatable read ResultSet

RepeatableReadResultSetProxyLogic allows ResultSet to be consumed more than once.

builder
  // configure listeners
  .repeatableReadResultSet()
//  .proxyResultSet(new RepeatableReadResultSetProxyLogicFactory())  // same as above
  .build();

3.3.2. Repeatable read ResultSet with CachedRowSetResultSetProxyLogic

New result set proxy, CachedRowSetResultSetProxyLogic is introduced at version 1.4.7. CachedRowSetResultSetProxyLogic uses javax.sql.rowset.CachedRowSet as underlying implementation, which is a sub-interface of ResultSet and provides cursor scrollability in disconnected fashion.
Default CachedRowSet implementation is com.sun.rowset.CachedRowSetImpl for oracle JDK (note for com.sun package), which you can change via RowSetFactory (please see details on JDK documentation).

To use CachedRowSetResultSetProxyLogic for ResultSet proxy, simply supply its proxy-logic factory, CachedRowSetResultSetProxyLogicFactory, to ProxyDataSourceBuilder when enabling result set proxy.

builder
  // proxy for regular ResultSet
  .proxyResultSet(new RepeatableReadResultSetProxyLogicFactory())
   // proxy for generated-keys
  .autoRetrieveGeneratedKeys(false, new RepeatableReadResultSetProxyLogicFactory());
  .build();

3.3.3. Using a custom ResultSet proxy

The proxyResultSet and autoRetrieveGeneratedKeys methods on the ProxyDataSourceBuilder can take a ResultSetProxyLogicFactory factory implementation. If you want to provide custom logic for handling the ResultSet object, you can provide a custom implementation by providing a custom factory implementation.

For example, this code in the datasource-proxy pull request #98 provides a Spring’s SqlRowSet based ResultSet proxy implementation to cover some shortcomings from the com.sun.rowset.CachedRowSetImpl.

4. Listeners

Currently, datasource-proxy provides the following listeners:

  • QueryExecutionListener

  • MethodExecutionListener

  • JdbcLifecycleEventListener

4.1. QueryExecutionListener

QueryExecutionListener is called when query execution methods are invoked (for example: execute, executeBatch, executeQuery, and so on).

QueryExecutionListener:
void beforeQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList);

void afterQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList);

ExecutionInfo contains contextual information about query execution (statement type, batch, connection ID, and so on), and QueryInfo contains the actual query string and parameters.

For the afterQuery callback, ExecutionInfo contains additional information: the query result, the time it took to execute the query, and any thrown exception.

The number of queries and parameters depends on the type of query execution.

Execution Type # of Query # of Query Parameters

Statement

1

empty

Statement (batch)

N

empty

Prepared Statement

1

1

Prepared Statement (batch)

1

N

Callable Statement

1

1

Callable Statement (batch)

1

N

4.2. MethodExecutionListener

MethodExecutionListener intercepts JDBC API calls for the following types:

  • Connection, Statement, PreparedStatement, and CallableStatement: all methods

  • ResultSet: all methods when the result set proxy is enabled

  • ProxyDataSource: getConnection() method

4.2.1. Replace the invoking method and parameters

In MethodExecutionListener#beforeMethod(), you can change the invoking method and its parameters on the target JDBC object.

For example, in application code, it may call Statement#execute(String). However, to leverage generated-keys retrieval, you may want to call Statement#execute(String, int) instead without changing application code.

To update or replace the method and parameters, set the new method or arguments via MethodExecutionContext#setMethod() and MethodExecutionContext#setMethodArgs() inside MethodExecutionListener#beforeMethod().

For the above use case:

private static final Method EXECUTE_METHOD = Statement.class.getMethod("execute", String.class);
private static final Method EXECUTE_WITH_INT_METHOD = Statement.class.getMethod("execute", String.class, int.class);

@Override
public void beforeMethod(MethodExecutionContext executionContext) {

  // replace "Statement#execute(sql)" with "Statement#execute(sql, RETURN_GENERATED_KEYS)"
  if (EXECUTE_METHOD.equals(executionContext.getMethod())) {

    Object[] originalArgs = executionContext.getMethodArgs();
    Object[] newArgs = new Object[]{originalArgs[0], Statement.RETURN_GENERATED_KEYS};

    executionContext.setMethod(EXECUTE_WITH_INT_METHOD);  // new method
    executionContext.setMethodArgs(newArgs);  // new method parameters
  }

}

4.3. JdbcLifecycleEventListener

The JdbcLifecycleEventListener interface defines before/after method callbacks for all JDBC interfaces that datasource-proxy proxies (DataSource, Connection, Statement, PreparedStatement, CallableStatement, and ResultSet). It also defines before/after callbacks for method invocations and query executions, analogous to MethodExecutionListener and QueryExecutionListener.

To implement your listener, you can extend JdbcLifecycleEventListenerAdapter, which implements all interface methods with no-op implementations and lets you selectively override the callbacks you care about.

DataSource dataSource =
  ProxyDataSourceBuilder.create(actualDataSource)
    .listener(new JdbcLifecycleEventListenerAdapter() {
      @Override
      public void beforeGetConnection(MethodExecutionContext executionContext) {
        // implement the logic before DataSource#getConnection();
      }

      @Override
      public void afterRollback(MethodExecutionContext executionContext) {
        // implement the logic after Connection#rollback();
      }
    })
    .build();

4.3.1. Overloaded methods

Because JDBC API classes are hierarchical (for example, PreparedStatement extends Statement), some methods are overloaded. For example, PreparedStatement#execute overloads Statement#execute, and PreparedStatement#setInt and CallableStatement#setInt are both overloaded forms.

JdbcLifecycleEventListener callbacks are invoked by comparing the invoked method name. Therefore, in this example, [before|after]Execute and [before|after]setInt are called. If you need to know exactly which overload was invoked, check the invoked instance, method, or arguments to identify the actual method that was called.

5. Built-in Listeners

5.1. Query Logging Listener

The query logging listener is the most commonly used listener. It logs executed queries and their actual parameters to Commons Logging, SLF4J, JUL, or System.out.

Sample output

Name:MyProxy, Connection:1, Time:1, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["CREATE TABLE users(id INT, name VARCHAR(255))"], Params:[]
Name:MyProxy, Connection:2, Time:5, Success:True, Type:Prepared, Batch:True, QuerySize:1, BatchSize:2, Query:["INSERT INTO users (id, name) VALUES (?, ?)"], Params:[(1,foo),(2,bar)]
Key Value

Name

Name of ProxyDataSource

Connection

Connection ID

Time

How long the query took to execute, in milliseconds

Success

Whether the query execution succeeded

Type

Type of statement (Statement/Prepared/Callable)

Batch

Batch execution

QuerySize

Number of queries

BatchSize

Number of batch operations

Query

The query text

Params

Query parameters

ProxyDataSourceBuilder provides methods for easily configuring and registering these listeners.

builder
  .logQueryByCommons()   // log using Commons Logging
  .logQueryBySlf4j()     // log using SLF4J
  .logQueryByJUL()       // log using Java Util Logging
  .logQueryToSysOut()    // log using System.out

  .multiline()           // enable multiline output
  .build();
If you choose to use Commons Logging or SLF4J, add the corresponding dependency to your project.

By default, each log entry is printed on a single line. If the multiline() option is set, the log is written across multiple lines. Likewise, the json() option prints the output in JSON format.

Here is another example of a multiline log that formats the query using Hibernate formatting.

Datasource-proxy v1.9 added the formatQuery() method on ProxyDataSourceBuilder, which lets you specify a callback to modify the query before logging.

// v1.9 or above
Formatter formatter = FormatStyle.BASIC.getFormatter();
DataSource ds = ProxyDataSourceBuilder
                  .create(actualDataSource)
                  .formatQuery(formatter::format)
                  .multiline()
                  .logQueryToSysOut()
                  .build()
// prior to v1.9
QueryLogEntryCreator logEntryCreator = new DefaultQueryLogEntryCreator() {
  @Override
  protected String formatQuery(String query) {
    return FormatStyle.BASIC.getFormatter().format(query);  // use Hibernate formatter
  }
};

logEntryCreator.setMultiline(true);  // enable multiline output

// print to System.out
SystemOutQueryLoggingListener listener = new SystemOutQueryLoggingListener();
listener.setQueryLogEntryCreator(logEntryCreator);

return ProxyDataSourceBuilder
            .create(actualDataSource)
            .listener(listener)
            .build()

You can also filter which queries are logged by providing a LoggingFilter:

builder
  .logQueryBySlf4j()
  // Only log queries that are not sent to the 'audit' table
  .loggingFilter((execInfo, queryInfoList) ->
    queryInfoList.stream().noneMatch(q -> q.getQuery().contains("audit"))
  )

If no filter is set, all queries are logged by default.

5.2. Slow Query Logging Listener

There are two approaches to detecting slow queries:

During execution

Detect slow queries while they are running. A scheduled check fires once the query has run longer than the configured threshold, even if the query has not finished yet. SlowQueryListener uses this approach. This approach is sometimes called preemptive; however, it only notifies you about the query. The running query is NOT interrupted or canceled.

After execution

Detect slow queries after they have finished. The afterQuery callback compares the query’s elapsed time with the threshold. This is a simple approach that can be implemented with any QueryExecutionListener. This approach is sometimes called non-preemptive.

During execution (SlowQueryListener) After execution (afterQuery callback)

When it is triggered

When the threshold has passed while the query is still running

After the query has finished

Elapsed time in ExecutionInfo

Time at the moment of the check (approximately the threshold time)

Actual query execution time

Query result, success/failure

Not available

Available

Thread that runs the callback

Thread of the scheduled executor in SlowQueryListener

Thread that executed the query

Queries that hang or never return

Detected

Not detected

Overhead

Schedules a check for each query execution

Compares elapsed time

Both approaches can be used together. For example, use SlowQueryListener to get an early notice of long-running queries and use an afterQuery callback to record their actual execution time.

5.2.1. During execution

When a query takes longer than the configured threshold, SlowQueryListener invokes a callback method. The callback is invoked only once for the target query if it exceeds the threshold time.

Combining logging and slow query detection, the following built-in classes write slow query entries to their corresponding loggers:

  • CommonsSlowQueryListener

  • SLF4JSlowQueryListener

  • JULSlowQueryListener

  • SystemOutSlowQueryListener

ProxyDataSourceBuilder provides methods for configuring these listeners.

builder
  .logSlowQueryByCommons(1, TimeUnit.SECONDS)   // log slow queries using Commons Logging
  .logSlowQueryBySlf4j(1, TimeUnit.SECONDS)     // log slow queries using SLF4J
  .logSlowQueryByJUL(1, TimeUnit.SECONDS)       // log slow queries using Java Util Logging
  .logSlowQueryToSysOut(1, TimeUnit.SECONDS)    // log slow queries using System.out

  .multiline()           // enable multiline output
  .build();
Elapsed Time

SlowQueryListener triggers its callback when query execution exceeds the configured threshold.

For example, if the threshold is set to 1 minute and a query takes 5 minutes to complete, the query is logged when 1 minute has passed while the query is still running.
This is expected behavior. Because the query is still executing, the elapsed time in ExecutionInfo is the time at the moment of the check (about 1 minute in this example), not the actual execution time of the query (5 minutes).

To log slow queries with their actual execution time, use the after execution approach.

5.2.2. After execution

To log queries that exceeded the threshold with their actual execution time, you can customize the existing Query Logging Listener implementation instead of using SlowQueryListener.

Here is a sample implementation using SLF4JQueryLoggingListener:

long thresholdInMills = ...
SLF4JQueryLoggingListener listener = new SLF4JQueryLoggingListener() {
  @Override
  public void afterQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
    // call query logging logic only when it took more than the threshold
    if (thresholdInMills <= execInfo.getElapsedTime()) {
      super.afterQuery(execInfo, queryInfoList);
    }
  }
};

listener.setLogLevel(SLF4JLogLevel.WARN);

This implementation logs queries that took longer than the configured threshold AFTER the query execution has finished, so the actual query execution time is available.

To perform other actions for slow queries, such as recording metrics or sending notifications, register an afterQuery callback with ProxyDataSourceBuilder:

long thresholdInMills = ...
builder
  .afterQuery((execInfo, queryInfoList) -> {
    if (thresholdInMills <= execInfo.getElapsedTime()) {
      // slow query: log, record metrics, send notification, etc.
    }
  })
  .build();

5.3. Query Count Listener

DataSourceQueryCountListener collects statistics for executed queries, such as the number of query types (SELECT, INSERT, UPDATE, DELETE), statement types (Statement, Prepared, Callable), and more. This class also accepts a strategy for storing such metrics. The default strategy stores metrics in a thread-local value, which is expected to be reset at the end of the request-response lifecycle. Another strategy is SingleQueryCountHolder, which accumulates query metrics from all threads until the metrics are explicitly cleared.

builder.
  .countQuery()    // enable collecting query metrics
//  .countQuery(new SingleQueryCountHolder())  // enable and specify a query count holder
  .build();

Stored metrics can be retrieved by the following static method.

QueryCountHolder.get()
Semantics changes based on how metrics are stored - thread local vs global map. When thread local is chosen, you need to clean up the stored metrics at the end of request-response lifecycle. You can call QueryCountHolder.clear() explicitly. Or, if built-in query-count-logging-listeners(will be addressed below) are registered, they automatically call clear() after it logs the count stats.

Combination of count listener and logging, datasource-proxy has built-in support for logging current query count metrics to loggers(commons, slf4j, jul, or system.out). They are available in servlet Filter and ServletRequestListener implementation. Furthermore, there is a custom tag for jsp.

5.3.1. Query count logging with servlet Filter

  • CommonsQueryCountLoggingServletFilter

  • JULQueryCountLoggingServletFilter

  • SLF4JQueryCountLoggingServletFilter

  • SystemOutQueryCountLoggingServletFilter

QueryCountLoggerBuilder class helps constructing filter instance.

with web.xml:
<filter>
  <filter-name>queryCountFilter</filter-name>
  <filter-class>net.ttddyy.dsproxy.support.CommonsQueryCountLoggingServletFilter</filter-class>
  <init-param>  <!-- OPTIONAL -->
    <param-name>clearQueryCounter</param-name>
    <param-value>true</param-value>
  </init-param>
  <init-param>  <!-- OPTIONAL -->
    <param-name>logLevel</param-name>
    <param-value>INFO</param-value>
  </init-param>
</filter>

5.3.2. Query count logging with ServletRequestListener

  • CommonsQueryCountLoggingRequestListener

  • JULQueryCountLoggingRequestListener

  • SLF4JQueryCountLoggingRequestListener

  • SystemOutQueryCountLoggingRequestListener

  • QueryCounterClearServletRequestListener

with web.xml:
<context-param>  <!-- OPTIONAL -->
  <param-name>queryCountCommonsLogLevel</param-name>
  <param-value>INFO</param-value>
</context-param>

<listener>
  <listener-class>net.ttddyy.dsproxy.support.CommonsQueryCountLoggingRequestListener</listener-class>
</listener>

5.3.3. Taglib Support

For jsp, a custom tag is supported to display query count metrics.

Declare custom tag:

<%@ taglib prefix="dsp" uri="http://www.ttddyy.net/dsproxy/tags" %>

When datasource is not specfied, total number of each datasource will be displayed.

<dsp:metrics metric="select"/>
<dsp:metrics metric="update"/>
<dsp:metrics metric="insert"/>
<dsp:metrics metric="delete"/>
<dsp:metrics metric="other"/>
<dsp:metrics metric="total"/>
<dsp:metrics metric="call"/>
<dsp:metrics metric="failure"/>
<dsp:metrics metric="time"/>

Specific datasource

<dsp:metrics metric="select" dataSource="FOO"/>
<dsp:metrics metric="update" dataSource="FOO"/>
<dsp:metrics metric="insert" dataSource="FOO"/>
<dsp:metrics metric="delete" dataSource="FOO"/>
<dsp:metrics metric="other" dataSource="FOO"/>
<dsp:metrics metric="total" dataSource="FOO"/>
<dsp:metrics metric="call" dataSource="FOO"/>
<dsp:metrics metric="failure" dataSource="FOO"/>
<dsp:metrics metric="time" dataSource="FOO"/>
Table 5. metric attribute
Name Description

select

Num of select queries

insert

Num of insert queries

update

Num of update queries

delete

Num of delete queries

other

Num of other queries

statement

Total num of statements

prepared

Total num of prepared statements

callable

Total num of callable statements

total

Total num of queries

success

Num of success queries

failure

Num of failure queries

time

Query execution time

5.4. Tracing Method Listener

TracingMethodListener is a method execution listener that prints out all JDBC API interaction.

Sample JDBC interaction using HSQL:

Code:
Connection conn = dataSource.getConnection()
PreparedStatement ps = conn.prepareStatement("INSERT INTO users (id, name) VALUES (?, ?)");
ps.setString(2, "FOO");
ps.setInt(1, 100);
ps.addBatch();
ps.setInt(1, 200);
ps.setString(2, "BAR");
ps.addBatch();
ps.executeBatch();
ps.close();
conn.close();
Output:
[1][success][0ms][conn=1] ProxyDataSource#getConnection()
[2][success][1ms][conn=1] JDBCConnection#prepareStatement("INSERT INTO users (id, name) VALUES (?, ?)")
[3][success][0ms][conn=1] JDBCPreparedStatement#setString(2,"FOO")
[4][success][0ms][conn=1] JDBCPreparedStatement#setInt(1,100)
[5][success][0ms][conn=1] JDBCPreparedStatement#addBatch()
[6][success][0ms][conn=1] JDBCPreparedStatement#setInt(1,200)
[7][success][0ms][conn=1] JDBCPreparedStatement#setString(2,"BAR")
[8][success][0ms][conn=1] JDBCPreparedStatement#addBatch()
[9][success][1ms][conn=1] JDBCPreparedStatement#executeBatch()
[10][success][0ms][conn=1] JDBCPreparedStatement#close()
[11][success][0ms][conn=1] JDBCConnection#close()

ProxyDataSourceBuilder provides following methods:

builder
  .traceMethods()
  .build();

To change output, the builder method also takes a string consumer.

builder
  // change the output to logger
  .traceMethods(message -> logger.debug(message))
  .build();

traceMethodsWhen method takes a boolean supplier to dynamically turn on/off tracing.

builder
  .traceMethodsWhen(() -> ...condition to perform tracing... )
  .build();

5.5. Custom Listeners

When you create a custom listener, you can register it via listener() or methodListener() methods.

  builder
      .listener(myQueryListener)          // register QueryExecutionListener
      .methodListener(myMethodListener)   // register MethodExecutionListener
      .build();

Also, with Java8 lambda, builder provides simple methods to inline listener definitions.

  • beforeQuery() and afterQuery() to register QueryExecutionListener

  • beforeMethod() and afterMethod() to register MethodExecutionListener

  builder
      // register MethodExecutionListener
      .afterMethod(executionContext -> {
        ....
      })
      // register QueryExecutionListener
      .afterQuery((execInfo, queryInfoList) -> {
          ...
      })

6. Sample Configuration

6.1. Use Case

  • Log all queries to System.out as JSON

  • Log query metrics to the Commons Logging logger at the end of each HTTP request using a servlet filter

6.2. pom.xml

Install datasource-proxy and commons-logging because this example uses Commons Logging for query metrics.

<dependency>
  <groupId>net.ttddyy</groupId>
  <artifactId>datasource-proxy</artifactId>
  <version>[LATEST-VERSION]</version>
</dependency>

<dependency>
  <groupId>commons-logging</groupId>
  <artifactId>commons-logging</artifactId>
  <version>[VERSION]</version>
</dependency>

6.3. web.xml

Add CommonsQueryCountLoggingServletFilter for query metrics.

  <filter>
    <filter-name>queryCountFilter</filter-name>
    <filter-class>net.ttddyy.dsproxy.support.CommonsQueryCountLoggingServletFilter</filter-class>
    <!-- Default to DEBUG level. Use the "logLevel" filter parameter to change the log level. -->
  </filter>

  <filter-mapping>
   <filter-name>queryCountFilter</filter-name>
   <url-pattern>*</url-pattern>
  </filter-mapping>

6.4. DataSource

Create a proxy datasource that performs the following actions:

  • Log query execution to System.out as JSON

  • Collect query metrics

Java-based example:
@Bean
public DataSource dataSource(DataSource actualDataSource) {
    return ProxyDataSourceBuilder
            .create(actualDataSource)
            .name("dataSource")
            .logQueryToSysOut()    // log query execution to System.out
            .asJson()
            .countQuery()          // collect query metrics
            .build();
}
XML-based example (applicationContext.xml):
<bean id="dataSource" primary="true" class="net.ttddyy.dsproxy.support.ProxyDataSource">
  <property name="dataSource" ref="actualDataSource"/>
  <property name="proxyConfig" ref="proxyConfig"/>
</bean>

<bean id="proxyConfig" factory-bean="proxyConfigSupport" factory-method="create"/>

<bean id="proxyConfigSupport" class="net.ttddyy.dsproxy.support.ProxyConfigSpringXmlSupport">
  <property name="dataSourceName" value="my-ds"/>
  <property name="queryListener" ref="queryListeners"/>
</bean>

<bean id="queryListeners" class="net.ttddyy.dsproxy.listener.ChainListener">
  <property name="listeners">
    <list>
      <bean class="net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener">
        <property name="writeAsJson" value="true" />
      </bean>
      <bean class="net.ttddyy.dsproxy.listener.DataSourceQueryCountListener"/>
    </list>
  </property>
</bean>

6.5. Controller

Write a simple controller that returns query metrics as JSON using Spring Boot.

@RestController
public class MyController {

    @RequestMapping
    public QueryCount index() throws Exception {
        // do some database operations
        return QueryCountHolder.getGrandTotal();
    }
}

6.6. Taglib (optional)

Display query metrics in a JSP page.

<%@ taglib prefix="dsp" uri="http://www.ttddyy.net/dsproxy/tags" %>

<dsp:metrics metric="select"/>  - Select
<dsp:metrics metric="update" dataSource="FOO" />  - Number of update queries for datasource FOO
<dsp:metrics metric="total"/>  - Total queries
<dsp:metrics metric="time"/>  - Total time

6.7. Accessing the application

> curl http://localhost:8080
{"select":1,"insert":0,"update":0,"delete":0,"other":0,"total":1,"failure":0,"success":1,"time":6}

6.8. More examples

See more examples in the datasource-proxy-examples project.

7. Development

7.1. Build Documentation

Generate index.html:

> ./mvnw asciidoctor:process-asciidoc@output-html

HTTP preview:

> ./mvnw asciidoctor:http@output-html

8. Changelog

1.11.1

Bug Fixes

  • Escape control characters in JSON query logs (Issue-134).
    Thanks 박재홍 for this contribution!!

1.11.0

New Features

Improvements

  • ProxyDataSource#close to unwrap and delegate close to underlying DataSource if needed (Issue-129).

    When ProxyDataSource#close is called, and the original DataSource does not implement Closeable or AutoCloseable, the proxy now attempts to unwrap the original DataSource to one that does, in order to delegate the close call appropriately.

    Thanks Réda Housni Alaoui for this contribution!!

1.10.1

Improvements

Bug Fixes

  • Do not create a proxy when ResultSet is null (Issue-117).

1.10

New Features

  • Added the ability to create a pure proxy DataSource object (Issue-102).

    The ProxyDataSource is a concrete implementation of the DataSource class, not a proxy object. To create pure proxies all the way from DataSource to ResultSet, a new method ProxyDataSourceBuilder#buildProxy() has been introduced. This buildProxy() method returns a proxy DataSource object and utilizes DataSourceProxyLogic to handle the proxy calls.

Improvements

  • Proxy instance is now retrievable from MethodExecutionContext#getProxy() (Issue-108).

  • Improved handling of equals for proxy objects (Issue-105).

  • Added Automatic-Module-Name (Issue-101).

  • Added ProxyJdbcObject#getProxyConfig (Issue-103).

  • Isolation level retrieval from the connection is now an opt-in feature (Issue-109).
    It is disabled by default and does not call Connection#getTransactionIsolation(). To enable isolation level retrieval, use ProxyDataSourceBuilder#retrieveIsolation(). ProxyDataSourceBuilder#writeIsolation() is effective only when ProxyDataSourceBuilder#retrieveIsolation() is called.

    ProxyDataSourceBuilder.create(ds).retrieveIsolation().writeIsolation().build();
  • Documentation about the Jakarta namespace (Issue-96).

Bug Fixes

  • Properly call the before/after method callback on ProxyDataSource#getConnection() (Issue-100).

1.9

New Features

  • Add formatQuery() method on ProxyDataSourceBuilder, which you can specify a callback to modify the query for logging. For example, apply a query formatter. (Issue-94)

    Formatter formatter = FormatStyle.BASIC.getFormatter();
    DataSource ds = ProxyDataSourceBuilder
                      .create(actualDataSource)
                      .formatQuery(formatter::format)
                      .logQueryToSysOut()
                      .build()

    Thanks Raphael Vullriede for this contribution!!

1.8.1

Improvements

  • Improve RepeatableReadResultSetProxyLogic

    • Support wasNull (Issue-87)

    • Support calling different numeric type get methods on the same index (Issue-88)

    Thanks Réda Housni Alaoui for this contribution!!

1.8

New Features

  • Add Isolation level in query logs. (Issue-74)

    Logging the isolation level is disabled by default.
    To enable, call logIsolationLevel(true) on ProxyDataSourceBuilder.

    ProxyDataSourceBuilder.create(datasource)
          .logQueryBySlf4j(SLF4JLogLevel.INFO)
          .logIsolationLevel(true)  // <==
          .build();

    Thanks Alexei Brinza for this contribution!!

  • Add support for Log4j2. (Issue-78)
    Thanks Ivan Jose Sanchez Pagador for this contribution!!

Improvements

  • Add ProxyDataSource#getDataSource() method to return the original data source. (Issue-81)

Bug Fixes

  • Support isWrapperFor and unwrap for ProxyDataSource itself (Issue-82)

1.7

Improvements

  • When asJson() is called on ProxyDataSourceBuilder, slow listeners also write the output as json. (Issue-66)

  • Support DataSource that implements AutoCloseable. (Issue-68)

1.6

Improvements

  • GlobalConnectionIdManager is added. This id-manager generates unique connection ids(sequentially increasing number) across datasources within JVM.

  • On JdbcLifecycleEventListener, method names have updated to map invoked method name only. Classname has removed in order to improve the interception for overloaded methods.

    The callback method is determined by invoked method name. See the details on below "Breaking Changes" section.

Breaking Changes

  • JdbcLifecycleEventListener method names have changed.
    (e.g.: beforeGetConnectionOnDataSource() ⇒ beforeGetConnection())

    Previously, the method names on the listener had convention of: [before|after]<MethodName>On<ClassName>
    e.g.: beforeGetConnectionOnDataSource()
    This has changed to [before|after]<MethodName>
    e.g.: beforeGetConnection()

    For overloaded methods (e.g.: Statement#execute and PreparedStatement#execute), please reference the documentation: Overloaded methods

1.5.1

New Features and Improvements

  • Add custom value store on ExecutionInfo and MethodExecutionContext Mainly used for passing values between before and after listener callback.

  • Stopwatch and StopwatchFactory are added for elapsed time calculation.
    Elapsed time calculation for query(ExecutionInfo#getElapsedTime) and method(MethodExecutionContext.getElapsedTime()) have been updated to use Stopwatch class.
    Stopwatch is created by StopwatchFactory, and it is configurable in ProxyConfig.
    Two implementation classes are available - SystemStopwatchFactory which creates SystemStopwatch and NanoTimeStopwatchFactory which creates NanoTimeStopwatch.
    SystemStopwatch uses System.currentTimeMillis() and NanoTimeStopwatch uses System.nanoTime() to calculate elapsed time. Default is set to SystemStopwatchFactory.

SlowQueryListener needs to set StopwatchFactory independently from ProxyConfig in order to calculate ExecutionInfo#getElapsedTime() for running slow queries.

Bug Fixes

  • Fix NullPointerException when Object methods(toString(), hashCode(), etc) is called on JdbcLifecycleEventListener.

1.5

  • JdbcLifecycleEventListener is added.
    This listener defines before/after callback methods for all of proxy classes(DataSource, Connection, Statement, PreparedStatement, CallableStatement, and ResultSet).
    See details on JdbcLifecycleEventListener.

1.4.10

  • Fix empty log line for getGeneratedKeys() and getResultSet() method calls. (Issue-54)

1.4.9

1.4.8

  • Populate ExecutionInfo#elapsedTime when query execution has failed.

  • Change ConnectionIdManager#getId() to return String instead of long.

1.4.7

1.4.6

  • Method and parameter replacement in MethodExecutionListener.
    See details on Replace the invoking method and parameters.

  • Updated Auto-retrieve generated keys feature. Auto-retrieval is now only called when key-generation is specified.
    Following methods specify key-generation:

    • Connection#prepareStatement(String, int[])

    • Connection#prepareStatement(String, String[])

    • Statement#execute(String, int[])

    • Statement#execute(String, String[])

    • Statement#executeUpdate(String, int[])

    • Statement#executeUpdate(String, String[])

    • Statement#executeLargeUpdate(String, int)

    • Statement#executeLargeUpdate(String, int[])

    • Statement#executeLargeUpdate(String, String[])

  • Make generated-keys auto-retrieval for batch execution configurable.

    • ProxyDataSourceBuilder#retrieveGeneratedKeysForBatch

      • batch execution for Statement: auto-retrieval is set to false to default

      • batch execution for Prepared/Callable: auto-retrieval is set to true to default

1.4.5

  • ConnectionInfo now has information whether the connection is closed or not.

  • Generated keys support.
    ResultSet for getGeneratedKeys() now has own configuration. See details on Generated Keys
    Thanks Réda Housni Alaoui(@reda-alaoui) for this contribution!!

1.4.4

  • Added TracingMethodListener that prints out JDBC API interaction. See details on Tracing Method Listener.

  • ProxyConfigSpringXmlSupport is added to support creating ProxyConfig bean in XML based spring config.

Sample XML config:
<bean id="proxyConfig"
      factory-bean="proxyConfigSupport"
      factory-method="create"/>

<bean id="proxyConfigSupport" class="net.ttddyy.dsproxy.support.ProxyConfigSpringXmlSupport">
  <property name="dataSourceName" value="my-ds"/>
  <property name="queryListener" ref="..."/>
  <property name="methodListener" ref="..."/>
</bean>

1.4.3

  • QueryLoggingListeners(Commons, SLF4J, JUL) added overridable loggingCondition callback(boolean supplier) that simply decides whether to skip entire logging logic based on the current log level set on its logger.

    e.g.: when SLF4JQueryLoggingListener writes SQL in DEBUG level, but the logger is set to INFO(more serious than DEBUG), then it will NOT perform logging logic including constructing log statement, etc.

  • Proxying ResultSet is refactored to align how other proxies are managed.

    Also, existing resultset-proxy is renamed to RepeatableReadResultSetProxyLogic. As part of refactoring, ResultSetProxyJdbcProxyFactory is removed. To enable proxying ResultSet, ProxyDataSourceBuilder now has #proxyResultSet() and #repeatableReadResultSet() methods.

    // before
    builder.jdbcProxyFactory(new ResultSetProxyJdbcProxyFactory()).build();
    // new
    builder.repeatableReadResultSet().build();  // or
    builder.proxyResultSet(new RepeatableReadResultSetProxyFactory()).build();
  • ProxyConfig is added to represent all proxy related configurations (datasource name, listeners, proxy factory, connection id manager). All values on InterceptorHolder are moved to ProxyConfig and InterceptorHolder class is removed.

  • MethodExecutionListener is added.

    MethodExecutionListener is a new type of listener that intercepts JDBC API calls: - Connection, Statement, PreparedStatement, CallableStatement: All methods - ResultSet: All methods when result set proxy is enabled. (ProxyDataSourceBuilder#[proxyResultSet()|repeatableReadResultSet()]) - ProxyDataSource: getConnection() method

    listeners can be registered via ProxyDataSourceBuilder#methodListener().

    builder.methodListener(myMethodListener).build();
  • ProxyDataSourceBuilder has added beforeMethod(), afterMethod(), beforeQuery(), and afterQuery() methods. These methods help inlining listener definitions especially with Java8 Lambda expression.

    ProxyDataSourceBuilder
      .create(actualDataSource)
      .name("MyDS")
      .proxyResultSet()  // apply listener on resultset
      // register MethodExecutionListener
      .afterMethod(executionContext -> {
          Method method = executionContext.getMethod();
          Class<?> targetClass = executionContext.getTarget().getClass();
          System.out.println(targetClass.getSimpleName() + "#" + method.getName());
      })
      // register QueryExecutionListener
      .afterQuery((execInfo, queryInfoList) -> {
          System.out.println("Query took " + execInfo.getElapsedTime() + "msec");
      })
      .build();

    sample output:

    # code:
    Connection conn = ds.getConnection();
    PreparedStatement ps = conn.prepareStatement("INSERT INTO users (id, name) VALUES (?, ?)");
    ps.setString(2, "FOO");
    ps.setInt(1, 3);
    ps.addBatch();
    ps.setInt(1, 4);
    ps.setString(2, "BAR");
    ps.addBatch();
    ps.executeBatch();
    ps.close();
    conn.close();
    # output:
    ProxyDataSource#getConnection
    JDBCConnection#prepareStatement
    JDBCPreparedStatement#setString
    JDBCPreparedStatement#setInt
    JDBCPreparedStatement#addBatch
    JDBCPreparedStatement#setInt
    JDBCPreparedStatement#setString
    JDBCPreparedStatement#addBatch
    JDBCPreparedStatement#executeBatch
    Query took 1msec
    JDBCPreparedStatement#close
    JDBCConnection#close

1.4.2

  • Assign connection ID on each connection

    When a connection is obtained from DataSource(DataSource.getConnection()), sequentially increasing unique number is assigned as its connection ID. (default implementation: DefaultConnectionIdManager) The connection ID is printed as Connection in logging.

  • Remove methods that don’t take dataSourceName on JdbcProxyFactory

    Instead, you need to specify null, empty String, or datasource name to the dataSourceName parameter. Following methods are removed:

    • Connection createConnection(Connection connection, InterceptorHolder interceptorHolder);

    • Statement createStatement(Statement statement, InterceptorHolder interceptorHolder);

    • PreparedStatement createPreparedStatement(PreparedStatement preparedStatement, String query, InterceptorHolder interceptorHolder);

  • DataSourceQueryCountListener now takes a strategy to resolve QueryCount.

    Default uses ThreadQueryCountHolder that uses thread local to hold QueryCount. This behaves same as before that the QueryCount holds per request counts(servlet request-response lifecycle).

    SingleQueryCountHolder uses single instance to hold count values. Therefore, this holds total accumulated values from all threads.

  • Update SlowQueryListener to use daemon threads as default.

    It is configurable by SlowQueryListener#setUseDaemonThread method.

1.4.1

  • Add setLog/setLogger to {Commons|SLF4J|JUL}QueryLoggingListener to allow users to set custom logger. Also added getters as well.

  • Update ~QueryCountLoggingServletFilter to allow configuring logger by name

  • Add query count logging implementation for JUL(Java Util Logging)

    • JULQueryCountLoggingHandlerInterceptor

    • JULQueryCountLoggingRequestListener

    • JULQueryCountLoggingServletFilter

  • Fix writing log with null in parameter set methods. (e.g: setString(1, null); )

  • Add SlowQueryListener that triggers callback method when query takes longer than specified threshold time.

    Also, added slow query logging listeners: CommonsSlowQueryListener JULSlowQueryListener SLF4JSlowQueryListener SystemOutSlowQueryListener

    In ProxyDataSourceBuilder, these methods are added: logSlowQueryByCommons() logSlowQueryByJUL() logSlowQueryBySlf4j() logSlowQueryToSysOut()

  • Add support to easily apply formatters on each query for logging.

    DefaultQueryLogEntryCreator#formatQuery() method has added. Subclass can override this method to provides formatted query.

    Example with BasicFormatterImpl in Hibernate.

    // set this instance to logging listeners
    public class PrettyQueryEntryCreator extends DefaultQueryLogEntryCreator {
      private Formatter formatter = FormatStyle.BASIC.getFormatter();  // from hibernate
    
      @Override
      protected String formatQuery(String query) {
        return this.formatter.format(query);
      }
    }
  • Add multiline output support for query logging.

    DefaultQueryLogEntryCreator now has setMultiline() method, and ProxyDataSourceBuilder also has added multiline() method. When multiline is enabled, logged query entries become multi lined.

    sample log output:

    Name:MyDS, Time:0, Success:True
    Type:Prepared, Batch:True, QuerySize:1, BatchSize:2
    Query:["INSERT INTO users (id, name) VALUES (?, ?)"]
    Params:[(1,foo),(2,bar)]

    set up with builder:

    DataSource dataSource =
        ProxyDataSourceBuilder
            .create(actualDataSource)
            .logQueryByCommons(INFO)
            .logSlowQueryByCommons(10, TimeUnit.MINUTES)
            .multiline()   // applies to both query logger and slow query logger
            .build();
  • Deprecate {Commons|SLF4J|JUL}QueryLoggingListener#resetLogger() methods.

    Use newly added setLog(String) or setLogger(String) method instead.

1.4

  • Move logging related listeners to sub package

    • from net.ttddyy.dsproxy.listener to net.ttddyy.dsproxy.listener.logging

  • classes for logging entry creation has been updated

    • QueryLogEntryCreator#getLogEntryAsJson has removed.

    • JSON style log entry creators is pulled up to DefaultJsonQueryLogEntryCreator

    • To use JSON style logging, you can set the QueryLogEntryCreator to [Commons|SLF4J|JUL|SystemOut]QueryLoggingListener#setQueryLogEntryCreator()

    • OracleOutputParameterLogEntryCreator has been split to OutputParameterLogEntryCreator and OutputParameterJsonLogEntryCreator

  • DefaultQueryLogEntryCreator#writeParamsForSingleEntry() has split to writeParamsEntryForSinglePreparedEntry() and writeParamsForSingleCallableEntry()

  • Do not include parameter index for logging prepared statement.

    Before(v1.3.3):

    ..., Params:[(1=10,2=foo),(1=20,2=bar)]
    ..., Params:[(1=30,2=FOO),(1=40,2=BAR)]
    ..., "params":[{"1":"10","2":"foo"},{"1":"20","2":"bar"}]}
    ..., "params":[{"1":"30","2":"FOO"},{"1":"40","2":"BAR"}]}

    Now:

    ..., Params:[(10,foo),(20,bar)]
    ..., Params:[(30,FOO),(40,BAR)]
    ..., "params":[["10","foo"],["20","bar"]]}
    ..., "params":[["30","FOO"],["40","BAR"]]}
  • Add JULQueryLoggingListener which uses JUL(Java Utils Logging) to log executed queries

  • Update logging for setNull and registerOutParameter to include sqltype

    e.g.: NULL(VARCHAR), OUTPUT(VARCHAR[12])

  • ResultSetProxyJdbcProxyFactory to create a proxy ResultSet that can be consumed more than once.

    Thanks Liam Williams for this contribution!!

  • QueryExecutionListener receives same instance of ExecutionInfo in beforeQuery and afterQuery methods

1.3.3

  • update DefaultQueryLogEntryCreator to allow subclasses to override log entry details

1.3.2

  • add CommonsOracleOutputParameterLoggingListener

  • add new listener for oracle to log output params. CommonsOracleOutputParameterLoggingListener

1.3.1

  • make logger name configurable in CommonsQueryLoggingListener and SLF4JQueryLoggingListener

  • setNull and registerOutParameter receives descriptive string value in QueryInfo#getQueryArgsList (temporal implementation)

  • ExecutionInfo will have access to the statement/prepared/callable object used by the execution

1.3

  • update minimum jdk to java6+

  • add java8 new jdbc API (JDBC 4.2)

  • new JNDI support class: ProxyDataSourceObjectFactory

  • new fluent API builder: ProxyDataSourceBuilder

  • logging:

    • update log format

    • add json format

    • more entries: statement-type, batch, batch-size

    • new logger for System.Out

  • change metric names: call ⇒ total, elapsedTime ⇒ time, added success, failure, etc.

  • rename ~QueryCountLoggingFilter to ~QueryCountServletFilter

  • remove deprecated methods

1.2.1

  • fixed prepared statement getting already executed queries in listener (Issue #9)

1.2

  • QueryTransformer and ParameterTransformer for query and parameter replacement