Audit Logging & SIEM Integration
Building an audit trail that stands up to scrutiny: what to record, a structured event format, hash-chained tamper evidence, Hibernate Envers, and shipping to a SIEM.
On this page
An audit log answers "who did what, when" long after the fact — for a compliance auditor, a security investigation, or a dispute. That purpose imposes requirements ordinary logging does not: completeness, immutability, and years of retention.
Key Takeaways
- Audit logs are separate from application logs, with different guarantees.
- Record who, what, when, from where, and the outcome — in a structured format.
- Hash-chain entries so tampering breaks the chain and is detectable.
- Write critical audit records in the same transaction as the action.
- Ship to a system the application cannot modify.
What to audit
Not everything, and not nothing. The events that matter for compliance and investigation:
| Category | Examples |
|---|---|
| Authentication | Login success and failure, MFA, logout, lockout |
| Authorisation | Access denied, privilege use, role changes |
| Data access | Who read which personal or sensitive record |
| Administrative | User CRUD, configuration changes, permission grants |
| Security events | Rate-limit breaches, blocked attacks, token anomalies |
| Financial | Transactions, refunds, balance changes |
The test for whether something belongs in the audit log: would you need it to answer a question in an investigation or an audit? A successful product-page view, no. A support agent viewing a customer's personal data, yes — that is exactly the "who accessed what" that access reviews demand.
The event format
public record AuditEvent(
String eventId,
Instant timestamp, // ISO 8601 with timezone, always UTC
String eventType, // "authentication.login.success"
String principal, // who — the subject performing the action
String sourceIp,
String userAgent,
String resourceType, // what kind of thing
String resourceId, // which specific thing
String action, // what was done
Outcome outcome, // SUCCESS or FAILURE
Map<String, Object> details,
String correlationId, // ties to the request that caused it
String previousHash // the tamper-evidence chain link
) { }Structured, not free text. "User alice deleted order 4711" is unqueryable at scale; the same information as fields lets a SIEM answer "every deletion by alice in March" or "everyone who accessed this record" instantly.
Timestamps must be UTC with an explicit offset. An audit log with local timestamps and no timezone is ambiguous exactly when precision matters — around a daylight-saving transition, or across regions.
Emitting events
@Service
public class AuditService {
@Transactional(propagation = Propagation.MANDATORY)
public void record(String eventType, String resourceType, String resourceId,
Action action, Outcome outcome, Map<String, Object> details) {
var auth = SecurityContextHolder.getContext().getAuthentication();
String previousHash = auditRepository.latestHash();
var event = new AuditEvent(
UUID.randomUUID().toString(),
Instant.now(),
eventType,
auth != null ? auth.getName() : "anonymous",
RequestContext.currentIp(),
RequestContext.currentUserAgent(),
resourceType, resourceId, action.name(), outcome,
details,
MDC.get("correlationId"),
previousHash);
// MANDATORY: this must run inside the caller's transaction, so the
// audit record and the audited action commit or roll back together.
// An action that succeeds while its audit record is lost is exactly
// the gap an attacker exploits.
auditRepository.append(event.withHash(computeHash(event)));
}
}The MANDATORY propagation is deliberate. For a financial transaction or a permission change, an audit
record that can be lost independently of the action defeats the purpose — the attacker's move is to
make the action succeed and the record disappear.
Use annotation-driven auditing for the common cases so it cannot be forgotten:
@Audited(type = "order.cancelled", resource = "order")
public void cancel(@AuditResourceId String orderId, String reason) { }Tamper evidence
private String computeHash(AuditEvent event) {
String canonical = String.join("|",
event.eventId(), event.timestamp().toString(), event.eventType(),
event.principal(), event.action(), event.outcome().name(),
event.previousHash()); // including the previous hash is what chains it
return DigestUtils.sha256Hex(canonical);
}
@Scheduled(cron = "0 0 * * * *")
public void verifyIntegrity() {
// Walk the chain and confirm each entry's previousHash matches the actual
// hash of its predecessor. A mismatch means an entry was altered or removed.
var breaks = auditRepository.verifyChain();
if (!breaks.isEmpty()) {
alerting.critical("audit log integrity failure at " + breaks);
}
}Hash chaining does not prevent tampering — it makes it detectable. Combined with append-only database
permissions (the application user can INSERT but not UPDATE or DELETE) and shipping to a
write-once store, silent alteration becomes very hard.
Hibernate Envers
For entity change history specifically, Envers automates the capture:
@Entity
@Audited
public class Customer {
@Id private UUID id;
private String email;
private CustomerTier tier;
}Envers creates a customer_AUD table recording every change with a revision number, and a REVINFO
table with who and when. Query historical state directly:
AuditReader reader = AuditReaderFactory.get(entityManager);
Customer asOfLastMonth = reader.find(Customer.class, customerId, revisionAtDate(lastMonth));This is complementary to the event log, not a replacement. Envers captures what changed in the data; the event log captures who did what action including reads and denied attempts, which leave no data change to record.
Shipping to a SIEM
// Audit events go to their own destination, not the application log stream,
// so a change to application logging cannot affect the audit trail.
@Component
public class SiemForwarder {
@EventListener
@Async("auditExecutor")
public void forward(AuditEvent event) {
// Structured JSON, one event per line, to the SIEM's ingest endpoint.
// A durable local buffer means a SIEM outage delays delivery rather
// than losing events.
siemBuffer.enqueue(objectMapper.writeValueAsString(event));
}
}The SIEM — Splunk, an ELK stack, a cloud equivalent — is where audit data becomes useful: correlation across events, dashboards for security review, and alerting on patterns like a burst of access-denied events or a privilege change outside a change window.
Ship to a destination the application cannot alter. If the same credentials that write the audit log can also delete from the SIEM, an attacker who compromises the application can erase their trail.
Retention
Audit logs are typically retained for years — often seven for financial data, sometimes longer by regulation. That is a different regime from application logs measured in weeks.
Tier the storage: recent events in a queryable hot store, older ones in cheap immutable object storage with a defined lifecycle. Object-lock or write-once-read-many storage prevents deletion before the retention period expires, which is both a tamper defence and a compliance requirement.
What to take away
Keep audit logs separate from application logs, structured, and complete. Record who, what, when, from where and the outcome. Write critical events in the same transaction as the action, hash-chain entries for tamper evidence, and ship to a store the application cannot modify. Retain for years in immutable storage, and alert on the patterns that signal an incident.
Frequently Asked Questions
What is the difference between audit logs and application logs?
How do I make an audit log tamper-evident?
Should audit logging be synchronous?
Related tutorials
- GDPR Compliance for Java ApplicationsImplementing the parts of GDPR that reach the code: data subject access and export, consent records, erasure through anonymisation, retention jobs, and breach notification.
- Zero-Trust ArchitectureBuilding zero-trust in practice: the NIST model, workload identity with SPIFFE, mTLS everywhere, per-request authorization, micro-segmentation, and just-in-time access.
- Cryptographic Key ManagementManaging keys across their lifecycle: generation, storage in HSMs and KMS, envelope encryption, rotation without re-encrypting everything, and the key hierarchy that makes it work.
- SecureX — Zero-Trust Platform ArchitectureThe capstone architecture: an auth server, security gateway, OPA policy engine, secrets service, audit service and self-service portal, and how they fit together.