Need advice on how to audit reason for change

Hello. I have a requirement to audit not only who makes a change and when but also the reason for the change. This is needed across almost all the entities in my project so I want to take the best approach possible. I please request any ideas on the best way to do this.

My idea is to add a reason attribute to all entities. Add a listener to all entities that:

  • Verifies reason is populated on insert or update
  • Saves the reason text and reference (entity class and ID) to a ReasonLog entity.
  • Clears out the reason text before saving the main entity.

I think this will get the job done but is clunky because it requires me to update all entities with a reason attribute and all edit screens with a reason field. Ideally, I wish there was a less intrusive way.

All ideas welcome. Thanks!

Hi,

I can suggest you to take a look at DynamicAttributes.
You can create required Dynamic Attribute for your entities, than create some listener (eth before update for all your entities or onBeforeCommitTransactionListener)
In case of onBeforeCommitTransactionListener you can check entity.getDynamicAttributes().containsKey("reason") (filtering system entities). Than add new ReasonLog entity.

Also it can be a good practice to create interface like Reasonable with default methods to get and save your ReasonLog. if(entity instanceof Reasonable) ((Reasonable)entity).commitReason()

Also you can create MappedSuperClass with non-persistent attribute and listener that saves your ReasonLog, but there is thonn of work to add attribute to every screen so dynamic attribute sounds better. IMHO

Hi,

I would recommend the approach with a non-persistent attribute in a MappedSuperclass. Dynamic attributes look overweight for the task because they will actually store (and load afterwards) the reason for each entity besides your ReasonLog. The non-persistent attribute content can be easily copied to the related ReasonLog instance in Before* entity listeners.

1 Like

Thanks guys for the great advice. I went down the path of MappedSuperclass with non-persistent attribute but ran into a problem. When assigning a value to the non-persistent attribute on the screen, the value does not always make it to the entity or transaction listeners. The value makes it to the Before Insert method but not the Before Update or Before Delete methods. Am I doing something wrong? Or is this a bug? Or is this intended functionality? I would like the transaction listener to get the non-persistent value in all cases so that I can log it to my reason log.

I attached a simple project to demonstrate the problem. Insert Car entity works but Update and Delete fail because the non-persistent reason attribute is null even when set on the screen.

Thanks.test-metaproperty.zip (85.6 KB)

Hi Keith,

Thank you for the test project.
The fact that you don’t see the value of the non-persistent attribute in BeforeUpdate/BeforeDelete entity listeners is a bug related to not handling the fields from superclasses. We’ll fix it in the next updates for 6.8+ which are scheduled for the end of this week.

As a workaround you could use BeforeAttachEntityListener which is invoked before merge and hence always has the reason attribute value, but in this listener there is no way to know if it is insert, update or delete, so it is probably not a viable solution for you.

Thank you very much for the quick turn around on this. I appreciate it.

Hello. Since others may have the need for this functionality, I created a sample project to show how I went about doing it. It also demonstrates the problem I’m having described below.reason.zip (88.4 KB)

Requirement

  • It is necessary not only to log the change of certain entities but also the reason for the change.
  • Changes include insert, update and delete of entity.
  • A consistent framework is needed.

Solution

  • When creating, updating and deleting relevant entities, a dialog is shown for the user to enter a reason for the change.
  • The change made to the entity, along with the reason is logged.

Key elements of sample reason project

  • Global module
    • com.company.reason.entity.Reasonable
      • Interface that has getReason method.
    • com.company.reason.entity.BaseUuidEntityWithReason
      • A mapped superclass.
      • Extends BaseUuidEntity (could be StandardEntity)
      • Implements Reasonable.
      • Has transient reason field.
    • com.company.reason.entity.Car
      • A sample entity that requires reason for updates.
      • Extends BaseUuidEntityWithReason.
      • Generic UI screens (edit and browse) are created for it.
  • Core module
    • com.company.reason.listener.BaseUuidEntityWithReasonEntityListener
      • Listens before delete, insert and update.
      • Make sure BaseUuidEntityWithReason is added as entity for this listener.
      • Make sure the reason attribute is populated before insert/update/delete.
      • Evaluates the entity after insert/update/delete and logs details along with reason.
      • Updating a ReasonLog entity would happen here but log file is used instead to simplify sample project.
      • This may not be necessary if the reason value can be captured by the standard EntityLog functionality (see problem below).
  • Web module
    • com.company.reason.web.dialog.ReasonDialog
      • A simple blank screen/dialog that asks the user to enter a reason text.
    • com.company.reason.gui.CustomRemoveAction
      • Extends RemoveAction.
      • Overrides remove method.
      • If selected entities are instance of Reasonable, then show the ReasonDialog to get a reason.
    • src/com/company/reason/web-spring.xml
      • Add bean entry to replace Cuba remove action with custom remove action.
    • com.company.reason.gui.components.CustomAbstractEditor
      • Extends AbstractEditor.
      • Overrides commitAndClose method.
      • Shows the reason dialog so that reason entered upon insert or update.
      • Have CarEdit (or any entity’s edit screen requiring a reason) extend this class.

To enforce reason auditing on any Entity:

  • Have the entity extend BaseUuidEntityWithReason.
  • Have the edit screen extend CustomAbstractEditor.

Problem:exclamation:

  • If I set up Entity Log to listen to changes to Car, it works fine for Create and Delete. The reason attribute is included in the log. But it does not work well for Modify because the reason attribute is not included in the details.
  • I tried to track down why but only got so far before losing my way. In AttributeChangeListener.internalPropertyChange, the owner variable does not include the reason value.
  • Is this a bug? It would be great if changes to transient attributes (null to any value) were recorded as a modification and make it to the entity log, just like create and delete.
  • If a fix is not possible, is there a workaround I can use? It would be great to leverage the existing EntityLog framework, rather than build my own entity and browser screen.:blush:

It’s certainly not a bug, because EntityLog cannot automatically log changes of non-persistent attributes by design. There is nothing to compare with for such attributes and hence no information whether it was changed and what was the previous value.
However, EntityLog can be invoked programmatically, and in this case you could pass the changes to it after computing them by your rules which could include non-persistent attributes as well. Unfortunately, the current API is not suitable for this, so I’ve created an issue that will enable it in the future.

Hi. Thank you for the enhancement in 6.10. I updated my BaseUuidEntityWithReasonEntityListener code with the following:

@Override
void onBeforeUpdate(BaseUuidEntityWithReason entity, EntityManager entityManager)
{
    if(entity.reason == null)
        throw new RuntimeException("A reason must be provided when updating.")

    entityLogAPI.registerModify(entity, true, getEntityAttributeChanges(entity))
}

private EntityAttributeChanges getEntityAttributeChanges(BaseUuidEntityWithReason entity)
{
	EntityAttributeChanges changes = new EntityAttributeChanges()
	def dirtyFields = persistenceTools.getDirtyFields(entity)
	dirtyFields.each {
		changes.addChange(it, persistenceTools.getOldValue(entity, it))
	}
	changes.addChange('reason', null)  //Assume system enforces reason value
	return changes
}

Now the entity log includes my non-persistent reason attribute. Please correct me if this is not the proper approach.

I noticed that if I only add the reason attribute to the EntityAttributeChanges object, other changed attributes are still included in the log but the old value is blank. I don’t mind the system putting in the persistent changed values but I need the old value in the log so I evaluated the dirty fields myself, which allowed the old value to be included. I guess the question is: is the addChange method meant to simply append a change to what the system has already evaluated? If so, then I’m not sure why the old value is not being logged. A bug or I am not using it properly?

Thanks again for the enhancement. Regardless of the intended functionality, I am able to accomplish what I need. Having built in functionality such as the Entity Log takes so much work off my plate. It is a terrific framework.

Updated project. reason.zip (88.6 KB)

Hi Keith,

I’m glad to hear that the framework helps you to deal with routine tasks - that is our goal.

You can simplify the code a bit, because there is a special method to collect changes of an entity:

private EntityAttributeChanges getEntityAttributeChanges(BaseUuidEntityWithReason entity)
{
	EntityAttributeChanges changes = new EntityAttributeChanges()
	changes.addChanges(entity)
	changes.addChange('reason', null)  //Assume system enforces reason value
	return changes
}
1 Like