Overview

Change log provides a built-in mechanism for logging changes (insert, update and delete events). There are 4 interfaces that we can optionally implement to control the various parts of the mechanism. By default, we can annotate entity beans with @ChangeLog and get the changes logged in JSON format to a logger.

There is overlap with @History which is a database centric approach. Unlike @ChangeLog @History is transactional and can NOT be bypassed.

Caveats

SqlUpdate, CallableSql and bulk updates via Update are not included in the change log.

Getting started

Step 1: Decide default for inserts

Inserts can default to being included in the change log or not and this will depend on the application. It is good to think about this up front as when we annotate the entity beans with @ChangeLog we can choose to override the default behaviour.

By default inserts are included. DatabaseConfig.setChangeLogIncludeInserts(boolean) can be used to control the default behaviour. This can also be set via application.properties.

ebean.changeLogIncludeInserts=false

Step 2: Add @ChangeLog

Add @ChangeLog annotation to all the entity beans that should have change logging on.

@ChangeLog
@Entity
public class Address {
...

/**
 * Only include updates if specific properties are changed.
 */
@ChangeLog(updatesThatInclude = {"name","dateOfBirth"})
@Entity
public class Customer {
...


/**
 * Override the default behaviour for inserts - INCLUDE, EXCLUDE or DEFAULT.
 * In this case exclude inserts from the change log.
 */
@ChangeLog(inserts = ChangeLogInsertMode.EXCLUDE)
@Entity
public class Customer {
...

Step 3: Implement ChangeLogPrepare

If we skip this step and don't supply a ChangeLogPrepare implementation a 'no op' implementation is used and the user context information (user id, user ip address etc) is left unpopulated.

Typically we implement ChangeLogPrepare obtaining the user context information such as user id and user ip address, setting that on the change set. Returning true indicates that the processing continues and the changeSet is passed to the ChangeLogListener in a background thread.

If we want logging to occur in the foreground we can invoke the logging in prepare method and return false (and this means the change set is not passed to the ChangeLogListener in a background thread).

class MyChangeLogPrepare implements ChangeLogPrepare {

  @Override
  public boolean prepare(ChangeSet changes) {

    // get user context information typically from a
    // ThreadLocal or similar mechanism

    String currentUserId = ...;
    changes.setUserId(currentUserId);

    String userIpAddress = ...;
    changes.setUserIpAddress(userIpAddress);

    changes.setSource("myApplicationName");

    // add arbitrary user context information to the
    // userContext map
    changes.getUserContext().put("some", "thing");

    return true;
  }
}

Step 4: Register ChangeLogPrepare implementation

The implementation of ChangeLogPrepare can be automatically detected if classpath scanning is on (just like entity beans are found). That is, if scanning is on we don't need to explicitly register the ChangeLogPrepare implementation and instead it will be found and instantiated.

If scanning is not used or the ChangeLogPrepare implementation has dependencies and its instantiation should be performed externally to Ebean then we register it explicitly with DatabaseConfig.

// example code explicitly registering the ChangeLogPrepare implementation

MyChangeLogPrepare changeLogPrepare = ...;

DatabaseConfig config = new DatabaseConfig();
...
// register explicitly here
config.setChangeLogPrepare(changeLogPrepare);


Database database = DatabaseFactory.create(config);
...

Step 5: Configure logging

The default implementation of ChangeLogListener logs events to io.ebean.ChangeLog. Typically, we would look to configure logging such that these logs go to a separate log.

Below in logback xml configuration an appender CHANGE_LOG for logging the change events to this separate log.

<appender name="CHANGE_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
  <File>log/changeLog.log</File>
  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <FileNamePattern>log/changeLog.log.%d{yyyy-MM-dd}</FileNamePattern>
    <MaxHistory>90</MaxHistory>
  </rollingPolicy>
  <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
    <pattern>%d{HH:mm:ss.SSS} %msg%n</pattern>
  </encoder>
</appender>

<logger name="io.ebean.ChangeLog" level="TRACE" additivity="false">
  <appender-ref ref="CHANGE_LOG"/>
</logger>