Enhancing Transparency and Accountability: Implementing Entity Audit Logging in Java [Part-1]
Tags: programming, java
Published: 2024-06-22 09:58:10

In the dynamic world of Java development, I embarked on a mission to fulfil a client’s request for comprehensive user activity logs across our company’s services. To achieve this, I turned to Ebean’s changelog feature, a robust tool for audit logging in Java applications.

Initially, I experimented with manual logging by modifying the save method of our model entities. However, this approach proved cumbersome and prone to errors, lacking the comprehensive tracking capabilities we required. Additionally, I explored Ebean’s History annotation, hoping it would streamline the audit logging process. Unfortunately, it didn’t meet our needs as it lacked the granularity and customisation options necessary for our application.

Undeterred, I focused on leveraging Ebean’s changelog functionality to seamlessly integrate audit logging into our model entities. Each modification became a part of a detailed activity log, providing insight into users’ actions across our services.

As the development progressed, the application transformed into a testament to our commitment to data integrity. Stakeholders applauded the newfound visibility into user activity, while compliance requirements were effortlessly met. Through the power of Ebean’s Changelog and my dedication to meeting client needs, our Java application emerged as a beacon of transparency, paving the way for a more accountable future in our services.


## BASIC ENTITY AUDIT LOGS

To enable audit logging for an entity we need to first enable `@ChangeLog` annotation from `io.ebean.annotation` on the entity. By default inserts are included. It can be excluded if required using `inserts = ChangeLogInsertMode.EXCLUDE` option in the annotation.

```
@Entity
@ChangeLog
@Table(name = "datagroup")
public class DataGroup extends Model {
  ...
@ChangeLog(inserts = ChangeLogInsertMode.EXCLUDE)
@Entity
public class Dataset extends Model {
```

If you want to log entity change only on certain field changes, then we can use
```
@ChangeLog(updatesThatInclude = {"field1","field2"})
```

Then we need to define a log appender for ChangeLogs in logback.xml. In our example we are using a ConsoleAppender to log out to STDOUT, however there are many options to log to socket, Files, DB, Kafka, SMTP or even slack and other apps. Refer [this link](https://logging.apache.org/log4j/2.x/manual/appenders.html) for various appenders from apache and its not limited to this. There are other libraries offering more options as well.
```
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>%date{yyyy-MM-dd HH:mm:ss} %coloredLevel %logger{15} - %message%n%xException{10}</pattern>
    </encoder>
</appender>
<logger name="io.ebean.ChangeLog" level="TRACE" additivity="false">
    <appender-ref ref="STDOUT"/>
</logger>
```
This is sufficient to log entity changes (inserts and updates).

## AUDIT LOGS WITH USER CONTEXT

To log user context or additional info along with the changes to the entity, we need to implement ChangeLogPrepare class to add the additional info to the change set. 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. We can also register it explicitly in DatabaseConfig as used in [this doc](https://ebean.io/docs/features/changelog).

For our use-case, we’ll keep the implementations inside models folder.

```
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;
  }
}
```

In the next post, we'll take a look into how all this fits in an asynchronous application where execution contexts can change frequently and the change context might get lost in thread switching.