Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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.

Advanced6 min readUpdated
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:

CategoryExamples
AuthenticationLogin success and failure, MFA, logout, lockout
AuthorisationAccess denied, privilege use, role changes
Data accessWho read which personal or sensitive record
AdministrativeUser CRUD, configuration changes, permission grants
Security eventsRate-limit breaches, blocked attacks, token anomalies
FinancialTransactions, 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

AuditEvent.java
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

AuditService.java
@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.java
@Audited(type = "order.cancelled", resource = "order")
public void cancel(@AuditResourceId String orderId, String reason) { }

Tamper evidence

Each entry hashes the previous one. Altering any entry invalidates every entry after it, so silent tampering is impossible to hide.
HashChain.java
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:

AuditedEntity.java
@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:

Historical.java
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

AuditAppender.java
// 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?
Application logs are for debugging — verbose, transient, and fine to lose. Audit logs are a record of who did what, for compliance and investigation — they must be complete, immutable, retained for years, and separate from application logs so a log rotation or a debug change cannot affect them.
How do I make an audit log tamper-evident?
Chain the entries: each record includes a hash of the previous one, so altering any entry breaks the chain from that point forward. Combined with append-only permissions and shipping to a system the application cannot modify, this makes silent tampering detectable.
Should audit logging be synchronous?
For events that must never be lost — a financial transaction, a permission change — write the audit record in the same transaction as the action, so they commit or roll back together. For high-volume lower-stakes events, async with a durable buffer is acceptable.

Related tutorials