Skip to main content

Custom Audit Trail

Overview

Audit trail records explain who changed what, when, from where, and why. In TeaQL systems, audit customization should be connected to CustomUserContext because the context has the user identity, tenant, request metadata, locale, trace ID, and operation boundary.

Audit records are important for:

  • Compliance
  • Security review
  • Debugging production issues
  • Customer support
  • Data recovery
  • Workflow accountability

What Should Be Audited

Common audit targets:

EventExamples
CreateNew customer, order, invoice, permission
UpdateField changes, status transitions, ownership changes
DeleteLogical delete through markToRemove()
Permission changesRole assignment, access grant, access revoke
Sensitive readsViewing customer PII, payment data, compliance data
System operationsEnsure schema, import, export, scheduled job
Integration eventsExternal callback, message consumption, sync operation

A useful audit record should include:

FieldMeaning
traceIdRequest or operation trace
tenantTenant or business scope
operatorIdCurrent user or system identity
operatorNameDisplay name for review
operationCreate, update, delete, read, approve, reject
entityTypeDomain object type
entityIdInternal ID
commonIdBusiness-facing ID, if available
beforeValuePrevious value or field snapshot
afterValueNew value or field snapshot
reasonBusiness reason or workflow action
clientIpRequest source
createdAtAudit time

Use an audit service behind CustomUserContext:

public interface AuditTrailService {

void record(AuditEvent event, CustomUserContext userContext);
}

Expose it from the context:

public class CustomUserContext extends UserContext {

public AuditTrailService auditTrailService() {
return getBean(AuditTrailService.class);
}

public void audit(AuditEvent event) {
auditTrailService().record(event, this);
}
}

Building Audit Events

Example event shape:

public class AuditEvent {

private String operation;
private String entityType;
private Object entityId;
private String commonId;
private Map<String, Object> beforeValue;
private Map<String, Object> afterValue;
private String reason;

// getters, setters, builder methods
}

Populate context fields during recording:

public void record(AuditEvent event, CustomUserContext userContext) {
event.setTraceId(userContext.traceId());
event.setTenant(userContext.currentTenantCode());
event.setOperatorId(userContext.currentUserId());
event.setOperatorName(userContext.currentUserName());
event.setClientIp(userContext.clientIp());
event.setCreatedAt(Instant.now());

auditRepository.save(event);
}

Auditing Entity Writes

The safest place to audit writes is around entity persistence:

public void afterSave(Object entity, ChangeSet changeSet) {
AuditEvent event = AuditEvent.builder()
.operation(changeSet.operation())
.entityType(entity.getClass().getSimpleName())
.entityId(changeSet.entityId())
.beforeValue(changeSet.beforeValues())
.afterValue(changeSet.afterValues())
.build();

audit(event);
}

Adapt ChangeSet to the change-tracking mechanism used in your project.


Auditing Business Operations

Not every audit entry should be field-level. Some operations need business-level records:

public void approveOrder(CustomUserContext userContext, String orderId, String reason) {
Order order = Q.orders().filterById(orderId).comment("Query orders").purpose("Load data").executeForOne(userContext);
order.approve(reason);
order.auditAs("Save order").save(userContext);

userContext.audit(AuditEvent.builder()
.operation("APPROVE_ORDER")
.entityType("Order")
.entityId(order.getId())
.commonId(order.getOrderNumber())
.reason(reason)
.build());
}

Business-level audit records are easier for auditors and support teams to understand.


Sensitive Data Handling

Audit records should be useful, but they must not leak secrets.

Avoid storing:

  • Passwords
  • Tokens
  • Full payment card numbers
  • Unmasked identity documents
  • Large request bodies
  • Unnecessary personal data

Mask or omit sensitive fields:

public Object auditValue(String fieldName, Object value) {
if ("password".equals(fieldName) || "token".equals(fieldName)) {
return "***";
}
if ("phone".equals(fieldName)) {
return maskPhone(value.toString());
}
return value;
}

Storage Strategy

Common audit storage options:

StrategyUse Case
Same databaseSimple deployment, transaction consistency
Separate audit databaseLarge volume, retention policies
Append-only logCompliance and tamper resistance
Message queueAsynchronous audit processing
External SIEMSecurity monitoring

For critical compliance systems, prefer append-only or restricted-update audit storage.


Best Practices

  • Put audit identity in CustomUserContext.
  • Audit business operations, not only raw field changes.
  • Use trace ID to connect audit records with logs.
  • Mask sensitive values.
  • Keep audit records immutable.
  • Include tenant and operator identity.
  • Write audit records in the same transaction when consistency is required.
  • Use asynchronous audit only when the business can tolerate delayed records.

Testing Checklist

  • Create, update, and delete operations produce audit records.
  • markToRemove() creates a delete audit event.
  • Audit records include user, tenant, trace ID, and client IP.
  • Sensitive fields are masked.
  • Failed transactions do not leave misleading audit records.
  • Business operations such as approval or rejection are audit-visible.
  • Audit data can be queried by entity, operator, tenant, and time.

Summary

Audit customization is one of the strongest reasons to centralize behavior in CustomUserContext. The context gives every TeaQL operation the identity and request metadata needed to produce reliable, explainable, and compliance-friendly audit records.