Overview

Change log provides a built in mechanism for logging changes (insert, update and delete events). There are 4 interfaces that you can optionally implement to control the various parts of the mechanism. By default you 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. Rob will do a video on the various pros and cons between @ChangeLog and @History.

Caveats

SqlUpdate, CallableSql and bulks updates via Update are not yet included in the logging (get in contact to discuss this area).

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 a little bit up front as when you annotate the entity beans with @ChangeLog you can choose to override the default behaviour.

By default inserts are included. ServerConfig.setChangeLogIncludeInserts(boolean) can be used to control the default behaviour. This can also be set via ebean.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 you 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 you implement ChangeLogPrepare obtaining the user context information such as user id and user ip address etc and 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 you want logging to occur in the foreground you 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 etc). That is, if scanning is on you 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 you can register it explicitly with the ServerConfig.

// example code explicitly registering the ChangeLogPrepare implementation

MyChangeLogPrepare changeLogPrepare = ...;

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


EbeanServer server = EbeanServerFactory.create(config);
...

Step 5: Configure logging

The default implementation of ChangeLogListener logs events to org.avaje.ebean.ChangeLog. Typically you 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.

<!-- LOGBACK configuration: separate logger for the change 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="org.avaje.ebean.ChangeLog" level="TRACE" additivity="false">
  <appender-ref ref="CHANGE_LOG"/>
</logger>