or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

audit.mdbuiltin-endpoints.mdendpoint-framework.mdhttp-exchanges.mdindex.mdinfo-contributors.mdjmx-integration.mdmanagement-operations.mdoperation-invocation.mdsanitization.mdsecurity.mdweb-integration.md
tile.json

audit.mddocs/

Audit Framework

QUICK REFERENCE

Key Classes and Packages

// Core Audit Types
org.springframework.boot.actuate.audit.AuditEvent
org.springframework.boot.actuate.audit.AuditEventRepository
org.springframework.boot.actuate.audit.InMemoryAuditEventRepository
org.springframework.boot.actuate.audit.AuditEventsEndpoint

// Spring Event Integration
org.springframework.boot.actuate.audit.listener.AuditApplicationEvent
org.springframework.boot.actuate.audit.listener.AbstractAuditListener
org.springframework.boot.actuate.audit.listener.AuditListener

// Security Integration
org.springframework.boot.actuate.security.AbstractAuthenticationAuditListener
org.springframework.boot.actuate.security.AuthenticationAuditListener
org.springframework.boot.actuate.security.AbstractAuthorizationAuditListener
org.springframework.boot.actuate.security.AuthorizationAuditListener

Audit Strategy Selector

What needs auditing?
│
├─ Security events (login, logout, access denied)?
│  ├─ Authentication events → AuthenticationAuditListener
│  └─ Authorization events → AuthorizationAuditListener
│
├─ Business events (data changes, operations)?
│  └─ Custom AuditApplicationEvent + AuditListener
│
├─ Compliance tracking?
│  └─ Persistent AuditEventRepository (database)
│
└─ Temporary monitoring?
   └─ InMemoryAuditEventRepository (capacity-limited)

Repository Selection Guide

Repository TypeCapacityPersistenceUse When
InMemoryAuditEventRepositoryLimited (configurable)NoDevelopment, testing
Custom JDBC/JPA RepositoryUnlimitedYesProduction, compliance
Custom External RepositoryUnlimitedYesCentralized logging

Common Event Types

// Authentication
AuthenticationAuditListener.AUTHENTICATION_SUCCESS
AuthenticationAuditListener.AUTHENTICATION_FAILURE
AuthenticationAuditListener.AUTHENTICATION_SWITCH
AuthenticationAuditListener.LOGOUT_SUCCESS

// Authorization
AuthorizationAuditListener.AUTHORIZATION_FAILURE

// Custom
"DATA_EXPORT", "CONFIG_CHANGE", "ADMIN_ACTION", "BULK_DELETE"

AGENT GUIDANCE

When to Use Audit Events

Always audit:

  • Authentication attempts (success/failure)
  • Authorization failures (access denied)
  • Privileged operations (admin actions)
  • Data modifications (create, update, delete)
  • Configuration changes
  • Security violations
  • Sensitive data access

Consider auditing:

  • API calls to sensitive endpoints
  • Bulk operations
  • Data exports
  • User profile changes
  • System state changes

Don't audit:

  • Read operations on public data
  • Health checks and monitoring pings
  • Static resource access
  • Routine background jobs

Pattern vs Anti-Pattern

PATTERN: Use built-in security listeners

// ✓ Automatic security event auditing
@Bean
public AuthenticationAuditListener authenticationAuditListener() {
    return new AuthenticationAuditListener();
}

@Bean
public AuthorizationAuditListener authorizationAuditListener() {
    return new AuthorizationAuditListener();
}

ANTI-PATTERN: Manual security event tracking

// ❌ Reinventing the wheel
@Component
public class ManualSecurityAuditor {
    @EventListener
    public void onLogin(AuthenticationSuccessEvent event) {
        // Manual audit event creation
        // Already handled by AuthenticationAuditListener
    }
}

PATTERN: Structured event data

// ✓ Structured, queryable data
AuditEvent event = new AuditEvent(
    principal,
    "DATA_EXPORT",
    Map.of(
        "recordCount", 1000,
        "format", "CSV",
        "dataType", "users"
    )
);

ANTI-PATTERN: Unstructured event data

// ❌ Hard to query
AuditEvent event = new AuditEvent(
    principal,
    "DATA_EXPORT",
    "exported 1000 users as CSV"  // String instead of Map
);

PATTERN: Use Spring events for decoupling

// ✓ Decoupled - service doesn't know about audit
@Service
public class UserService {
    private final ApplicationEventPublisher eventPublisher;

    public void deleteUser(String userId) {
        // Business logic
        userRepository.delete(userId);

        // Publish event
        eventPublisher.publishEvent(new AuditApplicationEvent(
            currentUser(),
            "USER_DELETE",
            Map.of("userId", userId)
        ));
    }
}

ANTI-PATTERN: Direct audit repository coupling

// ❌ Tightly coupled
@Service
public class UserService {
    private final AuditEventRepository auditRepository;

    public void deleteUser(String userId) {
        userRepository.delete(userId);

        // Direct coupling to audit infrastructure
        auditRepository.add(new AuditEvent(...));
    }
}

The audit framework provides flexible event tracking and storage for recording security events, authentication attempts, authorization decisions, and custom application events. It includes a repository abstraction for persistence and Spring event integration for decoupled event publishing.

Capabilities

Core Audit Types

/**
 * Value object representing an audit event.
 *
 * Thread-safe: Immutable after construction
 * Nullability: Principal can be null, data values can be null
 * Serializable: Yes (for storage/transmission)
 * Since: Spring Boot 1.0+
 */
public class AuditEvent implements Serializable {

    /**
     * Create audit event for current time.
     *
     * @param principal User principal (nullable for anonymous)
     * @param type Event type (non-null)
     * @param data Event data map (values can be null)
     */
    public AuditEvent(@Nullable String principal, String type, Map<String, @Nullable Object> data);

    /**
     * Create audit event from name-value pairs.
     *
     * @param principal User principal (nullable)
     * @param type Event type (non-null)
     * @param data Event data as "key=value" or "key" strings
     */
    public AuditEvent(@Nullable String principal, String type, String... data);

    /**
     * Create audit event with specific timestamp.
     *
     * @param timestamp Event timestamp (non-null)
     * @param principal User principal (nullable)
     * @param type Event type (non-null)
     * @param data Event data (values can be null)
     */
    public AuditEvent(Instant timestamp, @Nullable String principal, String type,
                     Map<String, @Nullable Object> data);

    public Instant getTimestamp();
    /**
     * Returns the user principal responsible for the event or an empty string
     * if the principal is not available.
     * NOTE: Never returns null - returns "" (empty string) when principal is unavailable.
     */
    public String getPrincipal();
    public String getType();
    public Map<String, @Nullable Object> getData();
}

/**
 * Repository for storing and retrieving audit events.
 *
 * Thread-safe: Implementations must be thread-safe
 * Since: Spring Boot 1.0+
 */
public interface AuditEventRepository {

    /**
     * Log an audit event.
     *
     * @param event Event to store (non-null)
     */
    void add(AuditEvent event);

    /**
     * Find audit events matching criteria.
     *
     * @param principal Principal filter (null = all principals)
     * @param after Time filter (null = all times)
     * @param type Event type filter (null = all types)
     * @return List of matching events (never null, may be empty)
     */
    List<AuditEvent> find(@Nullable String principal, @Nullable Instant after, @Nullable String type);
}

/**
 * In-memory audit event repository.
 *
 * Thread-safe: Yes (synchronized internally)
 * Capacity: Configurable, default 1000
 * Eviction: FIFO when capacity reached
 * Since: Spring Boot 1.0+
 */
public class InMemoryAuditEventRepository implements AuditEventRepository {

    /**
     * Create with default capacity (1000).
     */
    public InMemoryAuditEventRepository();

    /**
     * Create with custom capacity.
     *
     * @param capacity Maximum events to store
     */
    public InMemoryAuditEventRepository(int capacity);

    /**
     * Set capacity (removes oldest if reduced).
     *
     * @param capacity New capacity
     */
    public void setCapacity(int capacity);

    @Override
    public void add(AuditEvent event);

    @Override
    public List<AuditEvent> find(@Nullable String principal, @Nullable Instant after, @Nullable String type);
}

/**
 * Actuator endpoint that exposes audit events.
 * Provides read-only access to audit event repository via HTTP/JMX.
 *
 * Thread-safe: Yes (delegates to repository)
 * Nullability: All parameters nullable
 * Since: Spring Boot 2.0.0
 */
@Endpoint(id = "auditevents")
public class AuditEventsEndpoint {

    /**
     * Create endpoint with audit event repository.
     *
     * @param auditEventRepository Repository to query (non-null)
     */
    public AuditEventsEndpoint(AuditEventRepository auditEventRepository);

    /**
     * Retrieve audit events matching criteria.
     * All parameters are optional filters.
     *
     * @param principal Filter by principal name (nullable)
     * @param after Filter by events after this time (nullable)
     * @param type Filter by event type (nullable)
     * @return Descriptor containing matching events (never null)
     */
    @ReadOperation
    public AuditEventsDescriptor events(@Nullable String principal,
                                       @Nullable OffsetDateTime after,
                                       @Nullable String type);

    /**
     * Response body for audit events endpoint.
     * Wraps list of audit events for serialization.
     */
    public static final class AuditEventsDescriptor implements OperationResponseBody {

        /**
         * Get the list of audit events.
         *
         * @return List of audit events (never null, may be empty)
         */
        public List<AuditEvent> getEvents();
    }
}

/**
 * Spring ApplicationEvent wrapper for AuditEvent.
 * Allows audit events to be published through Spring's event system.
 *
 * Thread-safe: Yes (immutable after construction)
 * Since: Spring Boot 1.0+
 */
public class AuditApplicationEvent extends ApplicationEvent {

    private final AuditEvent auditEvent;

    /**
     * Create a new {@link AuditApplicationEvent} that wraps a newly created
     * {@link AuditEvent}.
     *
     * @param principal the principal (nullable for anonymous)
     * @param type the event type (non-null)
     * @param data the event data (non-null, values can be null)
     * @see AuditEvent#AuditEvent(String, String, Map)
     */
    public AuditApplicationEvent(String principal, String type,
                                Map<String, @Nullable Object> data);

    /**
     * Create a new {@link AuditApplicationEvent} that wraps a newly created
     * {@link AuditEvent}.
     *
     * @param principal the principal (nullable)
     * @param type the event type (non-null)
     * @param data the event data (non-null)
     * @see AuditEvent#AuditEvent(String, String, String...)
     */
    public AuditApplicationEvent(String principal, String type, String... data);

    /**
     * Create a new {@link AuditApplicationEvent} that wraps a newly created
     * {@link AuditEvent}.
     *
     * @param timestamp the timestamp (non-null)
     * @param principal the principal (nullable)
     * @param type the event type (non-null)
     * @param data the event data (non-null, values can be null)
     * @see AuditEvent#AuditEvent(Instant, String, String, Map)
     */
    public AuditApplicationEvent(Instant timestamp, String principal, String type,
                                Map<String, @Nullable Object> data);

    /**
     * Create a new {@link AuditApplicationEvent} that wraps the specified
     * {@link AuditEvent}.
     *
     * @param auditEvent the source of this event (non-null)
     */
    public AuditApplicationEvent(AuditEvent auditEvent);

    /**
     * Get the audit event.
     *
     * @return the audit event (never null)
     */
    public AuditEvent getAuditEvent();
}

/**
 * Base class for audit event listeners.
 * Provides infrastructure for listening to audit events and storing them.
 *
 * Thread-safe: Yes
 * Since: Spring Boot 1.0+
 */
public abstract class AbstractAuditListener implements ApplicationListener<AuditApplicationEvent> {

    /**
     * Called when an audit application event is received.
     * Subclasses can override to perform additional processing.
     *
     * @param event the audit event (non-null)
     */
    protected abstract void onAuditEvent(AuditEvent event);

    @Override
    public void onApplicationEvent(AuditApplicationEvent event) {
        onAuditEvent(event.getAuditEvent());
    }
}

/**
 * Default audit listener that stores events in a repository.
 * Listens for AuditApplicationEvents and stores them in an AuditEventRepository.
 *
 * Thread-safe: Yes
 * Auto-configured: No (manual registration required)
 * Since: Spring Boot 1.0+
 */
public class AuditListener extends AbstractAuditListener {

    /**
     * Create audit listener with repository.
     *
     * @param auditEventRepository Repository for storing events (non-null)
     */
    public AuditListener(AuditEventRepository auditEventRepository);

    /**
     * Store audit event in repository.
     *
     * @param event Audit event to store (non-null)
     */
    @Override
    protected void onAuditEvent(AuditEvent event);
}

COMPLETE WORKING EXAMPLES

Example 1: Basic Security Audit Configuration

package com.example.actuator;

import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository;
import org.springframework.boot.actuate.audit.listener.AuditListener;
import org.springframework.boot.actuate.security.AuthenticationAuditListener;
import org.springframework.boot.actuate.security.AuthorizationAuditListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Configuration for audit framework with security event tracking.
 *
 * Automatically captures:
 * - Authentication success/failure
 * - Authorization failures
 * - Logout events
 * - User switching
 *
 * Thread-safe: Yes
 * Since: Application 1.0
 */
@Configuration
public class AuditConfiguration {

    /**
     * In-memory audit event repository.
     * For production, replace with persistent implementation.
     *
     * @return Audit repository with 5000 event capacity
     */
    @Bean
    public AuditEventRepository auditEventRepository() {
        InMemoryAuditEventRepository repository =
            new InMemoryAuditEventRepository(5000);
        return repository;
    }

    /**
     * Audit listener that stores events from ApplicationEventPublisher.
     *
     * @param repository Repository for storage
     * @return Configured audit listener
     */
    @Bean
    public AuditListener auditListener(AuditEventRepository repository) {
        return new AuditListener(repository);
    }

    /**
     * Authentication audit listener for Spring Security.
     * Captures: login, logout, authentication failures.
     *
     * @return Authentication audit listener
     */
    @Bean
    public AuthenticationAuditListener authenticationAuditListener() {
        return new AuthenticationAuditListener();
    }

    /**
     * Authorization audit listener for Spring Security.
     * Captures: access denied events.
     *
     * @return Authorization audit listener
     */
    @Bean
    public AuthorizationAuditListener authorizationAuditListener() {
        return new AuthorizationAuditListener();
    }
}

Example 2: Custom Business Event Auditing

package com.example.actuator;

import org.springframework.boot.actuate.audit.AuditApplicationEvent;
import org.springframework.boot.actuate.audit.listener.AbstractAuditListener;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

/**
 * Service demonstrating business event auditing.
 *
 * Thread-safe: Yes
 * Event publishing: Asynchronous via Spring events
 * Since: Application 1.0
 */
@Service
public class UserManagementService {

    private static final Logger logger = LoggerFactory.getLogger(UserManagementService.class);
    private final ApplicationEventPublisher eventPublisher;

    public UserManagementService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    /**
     * Delete user with audit trail.
     *
     * @param userId User ID to delete
     * @param deletedBy Principal performing deletion
     */
    public void deleteUser(String userId, String deletedBy) {
        // Business logic
        logger.info("Deleting user: {}", userId);

        // Publish audit event
        eventPublisher.publishEvent(new AuditApplicationEvent(
            deletedBy,
            "USER_DELETE",
            Map.of(
                "userId", userId,
                "action", "delete",
                "reason", "account_closure"
            )
        ));
    }

    /**
     * Export user data with audit trail.
     *
     * @param exportedBy Principal performing export
     * @param format Export format
     * @param recordCount Number of records
     */
    public void exportUserData(String exportedBy, String format, int recordCount) {
        // Business logic
        logger.info("Exporting {} users as {}", recordCount, format);

        // Publish audit event
        eventPublisher.publishEvent(new AuditApplicationEvent(
            exportedBy,
            "DATA_EXPORT",
            Map.of(
                "dataType", "users",
                "format", format,
                "recordCount", recordCount
            )
        ));
    }

    /**
     * Change user role with audit trail.
     *
     * @param userId User ID
     * @param oldRole Previous role
     * @param newRole New role
     * @param changedBy Principal making change
     */
    public void changeUserRole(String userId, String oldRole, String newRole, String changedBy) {
        // Business logic
        logger.info("Changing role for user {}: {} -> {}", userId, oldRole, newRole);

        // Publish audit event
        eventPublisher.publishEvent(new AuditApplicationEvent(
            changedBy,
            "ROLE_CHANGE",
            Map.of(
                "userId", userId,
                "oldRole", oldRole,
                "newRole", newRole
            )
        ));
    }
}

/**
 * Custom audit listener for business events.
 * Performs additional logging and alerting.
 *
 * Thread-safe: Yes
 * Since: Application 1.0
 */
@Component
public class BusinessAuditListener extends AbstractAuditListener {

    private static final Logger logger = LoggerFactory.getLogger(BusinessAuditListener.class);

    @Override
    protected void onAuditEvent(AuditEvent event) {
        // Log all audit events
        logger.info("Audit Event: type={}, principal={}, data={}",
            event.getType(), event.getPrincipal(), event.getData());

        // Alert on sensitive operations
        if ("USER_DELETE".equals(event.getType()) ||
            "DATA_EXPORT".equals(event.getType())) {
            alertSecurityTeam(event);
        }

        // Check for suspicious patterns
        if (isHighRiskOperation(event)) {
            logger.warn("High-risk operation detected: {}", event);
        }
    }

    private void alertSecurityTeam(AuditEvent event) {
        // Send alert to security monitoring system
        logger.warn("SECURITY ALERT: {} by {}", event.getType(), event.getPrincipal());
    }

    private boolean isHighRiskOperation(AuditEvent event) {
        // Check for patterns indicating risk
        return "ROLE_CHANGE".equals(event.getType()) &&
               "ADMIN".equals(event.getData().get("newRole"));
    }
}

Example 3: Persistent Audit Repository

package com.example.actuator;

import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.jspecify.annotations.Nullable;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * JDBC-based persistent audit event repository.
 *
 * Thread-safe: Yes (via JdbcTemplate)
 * Capacity: Unlimited
 * Persistence: Database
 * Since: Application 1.0
 *
 * Database schema:
 * CREATE TABLE audit_events (
 *   id BIGINT PRIMARY KEY AUTO_INCREMENT,
 *   timestamp TIMESTAMP NOT NULL,
 *   principal VARCHAR(255),
 *   type VARCHAR(100) NOT NULL,
 *   data TEXT,
 *   INDEX idx_principal (principal),
 *   INDEX idx_timestamp (timestamp),
 *   INDEX idx_type (type)
 * );
 */
@Repository
public class JdbcAuditEventRepository implements AuditEventRepository {

    private final JdbcTemplate jdbcTemplate;

    public JdbcAuditEventRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void add(AuditEvent event) {
        String sql = "INSERT INTO audit_events (timestamp, principal, type, data) VALUES (?, ?, ?, ?)";

        jdbcTemplate.update(sql,
            Timestamp.from(event.getTimestamp()),
            event.getPrincipal(),
            event.getType(),
            serializeData(event.getData())
        );
    }

    @Override
    public List<AuditEvent> find(@Nullable String principal,
                                  @Nullable Instant after,
                                  @Nullable String type) {
        StringBuilder sql = new StringBuilder("SELECT * FROM audit_events WHERE 1=1");
        List<Object> params = new java.util.ArrayList<>();

        if (principal != null) {
            sql.append(" AND principal = ?");
            params.add(principal);
        }

        if (after != null) {
            sql.append(" AND timestamp > ?");
            params.add(Timestamp.from(after));
        }

        if (type != null) {
            sql.append(" AND type = ?");
            params.add(type);
        }

        sql.append(" ORDER BY timestamp DESC LIMIT 1000");

        return jdbcTemplate.query(sql.toString(), params.toArray(), new AuditEventRowMapper());
    }

    private String serializeData(Map<String, Object> data) {
        // Serialize to JSON or other format
        return data.toString(); // Simplified - use Jackson in real implementation
    }

    private Map<String, Object> deserializeData(String data) {
        // Deserialize from JSON or other format
        return new HashMap<>(); // Simplified - use Jackson in real implementation
    }

    private class AuditEventRowMapper implements RowMapper<AuditEvent> {
        @Override
        public AuditEvent mapRow(ResultSet rs, int rowNum) throws SQLException {
            return new AuditEvent(
                rs.getTimestamp("timestamp").toInstant(),
                rs.getString("principal"),
                rs.getString("type"),
                deserializeData(rs.getString("data"))
            );
        }
    }
}

TESTING EXAMPLES

Test 1: Basic Audit Event Creation

package com.example.actuator;

import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.audit.AuditEvent;
import java.time.Instant;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;

class AuditEventTest {

    @Test
    void createEvent_WithCurrentTime_SetsTimestamp() {
        Instant before = Instant.now();

        AuditEvent event = new AuditEvent(
            "testuser",
            "TEST_EVENT",
            Map.of("key", "value")
        );

        Instant after = Instant.now();

        assertThat(event.getTimestamp()).isBetween(before, after);
        assertThat(event.getPrincipal()).isEqualTo("testuser");
        assertThat(event.getType()).isEqualTo("TEST_EVENT");
        assertThat(event.getData()).containsEntry("key", "value");
    }

    @Test
    void createEvent_WithNameValuePairs_ParsesData() {
        AuditEvent event = new AuditEvent(
            "testuser",
            "TEST_EVENT",
            "key1=value1", "key2=value2", "key3"
        );

        assertThat(event.getData()).containsKeys("key1", "key2", "key3");
        assertThat(event.getData().get("key1")).isEqualTo("value1");
        assertThat(event.getData().get("key2")).isEqualTo("value2");
    }

    @Test
    void createEvent_WithNullPrincipal_ReturnsEmptyString() {
        AuditEvent event = new AuditEvent(
            null,
            "TEST_EVENT",
            Map.of()
        );

        assertThat(event.getPrincipal()).isEmpty();
    }
}

Test 2: In-Memory Repository

package com.example.actuator;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository;
import java.time.Instant;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;

class InMemoryAuditEventRepositoryTest {

    private InMemoryAuditEventRepository repository;

    @BeforeEach
    void setUp() {
        repository = new InMemoryAuditEventRepository(100);
    }

    @Test
    void add_StoresEvent() {
        AuditEvent event = new AuditEvent("user", "TEST", Map.of());

        repository.add(event);

        List<AuditEvent> found = repository.find("user", null, null);
        assertThat(found).hasSize(1);
        assertThat(found.get(0).getPrincipal()).isEqualTo("user");
    }

    @Test
    void find_ByPrincipal_FiltersCorrectly() {
        repository.add(new AuditEvent("user1", "TEST", Map.of()));
        repository.add(new AuditEvent("user2", "TEST", Map.of()));

        List<AuditEvent> found = repository.find("user1", null, null);

        assertThat(found).hasSize(1);
        assertThat(found.get(0).getPrincipal()).isEqualTo("user1");
    }

    @Test
    void find_ByTime_FiltersCorrectly() {
        Instant now = Instant.now();
        Instant past = now.minus(Duration.ofHours(1));

        repository.add(new AuditEvent(past, "user", "OLD_EVENT", Map.of()));
        repository.add(new AuditEvent(now, "user", "NEW_EVENT", Map.of()));

        List<AuditEvent> found = repository.find(null, now.minus(Duration.ofMinutes(30)), null);

        assertThat(found).hasSize(1);
        assertThat(found.get(0).getType()).isEqualTo("NEW_EVENT");
    }

    @Test
    void capacity_EvictsOldestEvents() {
        repository.setCapacity(2);

        repository.add(new AuditEvent("user", "EVENT1", Map.of()));
        repository.add(new AuditEvent("user", "EVENT2", Map.of()));
        repository.add(new AuditEvent("user", "EVENT3", Map.of()));

        List<AuditEvent> all = repository.find(null, null, null);

        assertThat(all).hasSize(2);
        assertThat(all).extracting(AuditEvent::getType)
            .containsExactlyInAnyOrder("EVENT2", "EVENT3");
    }
}

TROUBLESHOOTING

Common Error: Audit Events Not Recorded

Problem: No audit events appearing in repository

Causes:

  1. AuditListener bean not registered
  2. Events published incorrectly
  3. Repository not configured

Solutions:

// Solution 1: Register AuditListener
@Bean
public AuditListener auditListener(AuditEventRepository repository) {
    return new AuditListener(repository);
}

// Solution 2: Use AuditApplicationEvent
eventPublisher.publishEvent(new AuditApplicationEvent(
    principal,
    "EVENT_TYPE",
    Map.of("key", "value")
));

// Solution 3: Configure repository
@Bean
public AuditEventRepository auditEventRepository() {
    return new InMemoryAuditEventRepository();
}

Common Error: Security Events Not Captured

Problem: Login/logout events not audited

Causes:

  1. Security audit listeners not registered
  2. Spring Security not configured
  3. Events not published by security

Solutions:

// Register both listeners
@Bean
public AuthenticationAuditListener authenticationAuditListener() {
    return new AuthenticationAuditListener();
}

@Bean
public AuthorizationAuditListener authorizationAuditListener() {
    return new AuthorizationAuditListener();
}

// Ensure Spring Security publishes events (default behavior)

Common Error: Memory Overflow

Problem: Too many events in memory

Causes:

  1. InMemory repository with high capacity
  2. No eviction strategy
  3. High event volume

Solutions:

// Solution 1: Reduce capacity
InMemoryAuditEventRepository repository =
    new InMemoryAuditEventRepository(1000); // Lower capacity

// Solution 2: Use persistent repository
@Bean
public AuditEventRepository auditEventRepository(JdbcTemplate jdbc) {
    return new JdbcAuditEventRepository(jdbc);
}

// Solution 3: Add cleanup job
@Scheduled(fixedRate = 3600000) // Every hour
public void cleanupOldEvents() {
    jdbcTemplate.update(
        "DELETE FROM audit_events WHERE timestamp < ?",
        Timestamp.from(Instant.now().minus(Duration.ofDays(30)))
    );
}

PERFORMANCE NOTES

Repository Capacity Planning

// Calculate capacity based on event rate
// Example: 100 events/hour * 24 hours = 2400 events/day

@Bean
public InMemoryAuditEventRepository auditEventRepository() {
    // Store 7 days of events
    int eventsPerDay = 2400;
    int days = 7;
    int capacity = eventsPerDay * days;

    return new InMemoryAuditEventRepository(capacity);
}

Event Publishing Performance

// Audit event publishing is asynchronous
// No performance impact on business operations

@Service
public class PerformantService {

    private final ApplicationEventPublisher eventPublisher;

    public void criticalOperation() {
        // Business logic (synchronous)
        performCriticalWork();

        // Audit (asynchronous via Spring events)
        eventPublisher.publishEvent(new AuditApplicationEvent(...));
        // Returns immediately, event processed async
    }
}

Query Optimization

// For large audit tables, ensure proper indexing
/*
CREATE INDEX idx_audit_principal ON audit_events(principal);
CREATE INDEX idx_audit_timestamp ON audit_events(timestamp);
CREATE INDEX idx_audit_type ON audit_events(type);
CREATE INDEX idx_audit_composite ON audit_events(principal, timestamp, type);
*/

// Use pagination for large result sets
public List<AuditEvent> findPaginated(String principal, Instant after, int page, int size) {
    String sql = "SELECT * FROM audit_events WHERE principal = ? AND timestamp > ? " +
                 "ORDER BY timestamp DESC LIMIT ? OFFSET ?";
    return jdbcTemplate.query(sql, rowMapper, principal, after, size, page * size);
}

Cross-References

  • For endpoint exposure: Built-in Endpoints
  • For security integration: Security Integration
  • For HTTP request tracking: HTTP Exchange Tracking
  • For endpoint framework: Endpoint Framework