spring-audit-support

Logo

Audit Event Repositories

License


A repository is where audit events end up. This page covers the repositories the library supplies, what each of them requires, and how to wire one by hand.

Under the Spring Boot starter you normally do not wire anything. The repositories are created from properties, see Configuration. The manual wiring shown here is for applications not using the starter, and for when a repository needs something the properties do not expose.

The repository interface

Every repository implements ExtendedAuditEventRepository, which extends Spring Boot’s AuditEventRepository. So they plug straight into the actuator’s auditing, while adding event filtering and a richer query API.

Most repositories extend AbstractAuditEventRepository, which supplies two things common to all of them, described next.

Filtering

Each repository may be given a predicate deciding which events it stores. Build one with the static helpers on AbstractAuditEventRepository:

AbstractAuditEventRepository.exclusionPredicate(List.of("noisy_event_type"));
AbstractAuditEventRepository.inclusionPredicate(List.of("user_login", "user_logout"));
AbstractAuditEventRepository.inclusionExclusionPredicate(includeTypes, excludeTypes);

Note the difference between the first two and the third. inclusionPredicate is a literal whitelist, so an empty list accepts nothing. inclusionExclusionPredicate is meant for configuration, where an unset include list means “no inclusion constraint”, so an empty include list accepts everything not excluded. Exclusion always wins over inclusion.

When several repositories are combined, put the filter on the delegating repository instead of on each one, so the rule is stated once.

Write failures

A failure to write is always logged at ERROR. By default it additionally throws an AuditEventWriteException. Call setThrowOnWriteFail(false) to log and continue, so that a storage problem does not break the operation that triggered the audit event.

Which is right depends on the deployment. Throwing means an audit event is never silently lost. Continuing means a failing sink cannot take the service down. A common arrangement is to throw for the durable store and continue for the rest, which the delegating repository supports per delegate.

Read failures always propagate.

Querying

Two query paths exist:

Both return events most recent first.

Not every repository can be queried. supportsFind() reports whether predicate based queries can be served. The file and syslog repositories are write-only sinks, so they return false and their find methods return an empty list.

Repository Queryable
In-memory Yes
JDBC Yes
MongoDB Yes
Redis Yes
File No
Syslog No

In-memory

InMemoryAuditEventRepository keeps a bounded number of the most recent events in memory. Everything is lost when the application stops, so it is for development, for tests, and for making the actuator endpoint useful in an application whose only other repository is write-only.

It needs no dependencies and no configuration.

@Bean
AuditEventRepository auditEventRepository() {
  return new InMemoryAuditEventRepository(5000);
}

Because its window is small, it should always come last when combined with a durable repository. See Configuration.

File

FileBasedAuditEventRepository writes events to a file, one JSON event per line. It uses only the JDK, so no extra dependency is needed.

It is write-only and does not support querying.

Daily file rolling

The file is rolled per date in UTC. When the first event of a new day is written, the current file is renamed to <name>-<yyyyMMdd>.<ext> and a fresh file is started:

audit.log              <- today's events
audit-20260806.log     <- yesterday's events
audit-20260805.log

If the file name has no extension, the date is appended, so audit becomes audit-20260806.

Rolled files accumulate indefinitely. Prune them with your normal log retention tooling.

Wiring it up

The constructor throws IOException if the path is invalid, meaning it points to a directory or to an existing file that is not writable. Missing parent directories are created.

@Bean
AuditEventRepository auditEventRepository() throws IOException {
  final AuditEventMapper eventMapper = new JsonAuditEventMapper(JsonMapper.builder().build());
  return new FileBasedAuditEventRepository("/var/log/myapp/audit.log", eventMapper);
}

One writer per file. The repository backs the file with a Java Util Logging FileHandler, which also creates a <name>.lck lock file next to it. Use a single FileBasedAuditEventRepository instance per file within the JVM.

JDBC

DatabaseAuditEventRepository persists events to a database through an AuditEventDao. It contains no SQL and no schema knowledge of its own:

DatabaseAuditEventRepository  ──uses──▶  AuditEventDao  ◀──implemented by──  DefaultJdbcAuditEventDao
   (filtering, predicate find,             (the seam)                          (the default schema and SQL)
    ordering)

AuditEventDao has three operations expressed purely in terms of AuditEvent: save, find(principal, after, type) and findRecent(limit). Two marker sub-interfaces, JdbcAuditEventDao and MongoAuditEventDao, let the auto-configuration tell the two database backends apart.

How events are stored

Each event is one row. A few flat columns are used for querying, and the complete event is stored as JSON in event_data, which is what gets reconstructed on read. No information is lost, including nested data and extra root fields.

Column Purpose
id Auto-generated primary key, also the tiebreaker for ordering
event_time Event timestamp, stored in UTC
principal Initiator of the event
event_type For example system_alert
application_name The name of the application field of a structured event, null otherwise
application_version The version of the application field of a structured event, null otherwise
correlation_id From structured events, null otherwise
event_data The full event serialized as JSON

Note that the trace ID has no flat column. It is preserved in event_data like everything else, but it cannot be used as a query criterion.

Dependencies

DefaultJdbcAuditEventDao needs spring-jdbc, an optional dependency of this library, and a JDBC driver for your database:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
</dependency>

<!-- Your database driver, for example PostgreSQL -->
<dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
</dependency>

A custom AuditEventDao that does not use JdbcTemplate needs neither.

Schema

The table name defaults to audit_events and is configurable. The column names are fixed. The indexes are optional but recommended, since they back the find(principal, after, type) query.

PostgreSQL

CREATE TABLE audit_events (
    id                  BIGSERIAL PRIMARY KEY,
    event_time          TIMESTAMP NOT NULL,
    principal           VARCHAR(255),
    event_type          VARCHAR(255) NOT NULL,
    application_name    VARCHAR(255),
    application_version VARCHAR(255),
    correlation_id      VARCHAR(255),
    event_data          TEXT NOT NULL
);

CREATE INDEX idx_audit_events_time      ON audit_events (event_time);
CREATE INDEX idx_audit_events_principal ON audit_events (principal);
CREATE INDEX idx_audit_events_type      ON audit_events (event_type);

MySQL and MariaDB

CREATE TABLE audit_events (
    id                  BIGINT AUTO_INCREMENT PRIMARY KEY,
    event_time          DATETIME(6) NOT NULL,
    principal           VARCHAR(255),
    event_type          VARCHAR(255) NOT NULL,
    application_name    VARCHAR(255),
    application_version VARCHAR(255),
    correlation_id      VARCHAR(255),
    event_data          TEXT NOT NULL
);

CREATE INDEX idx_audit_events_time      ON audit_events (event_time);
CREATE INDEX idx_audit_events_principal ON audit_events (principal);
CREATE INDEX idx_audit_events_type      ON audit_events (event_type);

DATETIME(6) is used rather than TIMESTAMP to avoid MySQL’s year 2038 range limit and to keep microsecond precision.

H2, for testing

CREATE TABLE audit_events (
    id                  BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    event_time          TIMESTAMP NOT NULL,
    principal           VARCHAR(255),
    event_type          VARCHAR(255) NOT NULL,
    application_name    VARCHAR(255),
    application_version VARCHAR(255),
    correlation_id      VARCHAR(255),
    event_data          CLOB NOT NULL
);

Wiring it up

@Bean
AuditEventRepository auditEventRepository(final DataSource dataSource) {
  final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
  final AuditEventMapper eventMapper = new JsonAuditEventMapper(JsonMapper.builder().build());
  final AuditEventDao dao = new DefaultJdbcAuditEventDao(jdbcTemplate, eventMapper);
  return new DatabaseAuditEventRepository(dao);
}

If your table looks different, implement JdbcAuditEventDao and pass it to the same repository. See Configuration for doing this under the starter.

Retention

The table grows until you prune it:

DELETE FROM audit_events WHERE event_time < ?; -- for example older than 90 days

Database-native partitioning or time-to-live features work as well.

MongoDB

The same DatabaseAuditEventRepository as above, with DefaultMongoAuditEventDao as the DAO. It uses Spring Data MongoDB’s MongoOperations, typically a MongoTemplate.

How events are stored

Each event is one document, following the same principle as the JDBC backend: flat fields for querying, the complete event as JSON in eventData.

Field Purpose
_id Mongo-generated document id
eventTime Event timestamp, stored in UTC
principal Initiator of the event
eventType For example system_alert
applicationName The name of the application field of a structured event, null otherwise
applicationVersion The version of the application field of a structured event, null otherwise
correlationId From structured events, null otherwise
eventData The full event serialized as JSON

No schema or explicit collection creation is needed, since MongoDB creates the collection on first write. For efficient find(principal, after, type) queries, add indexes on the query fields:

db.audit_events.createIndex({ eventTime: -1 });
db.audit_events.createIndex({ principal: 1 });
db.audit_events.createIndex({ eventType: 1 });

Dependencies

spring-data-mongodb, an optional dependency of this library, plus a Mongo driver. The Spring Boot starter gives you both along with connection auto-configuration:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

You also need a configured MongoDB connection so that Spring Boot provides the MongoTemplate. See Spring Boot, MongoDB and the spring.data.mongodb.* properties.

Wiring it up

@Bean
AuditEventRepository auditEventRepository(final MongoTemplate mongoTemplate) {
  final AuditEventMapper eventMapper = new JsonAuditEventMapper(JsonMapper.builder().build());
  final AuditEventDao dao = new DefaultMongoAuditEventDao(mongoTemplate, eventMapper);
  return new DatabaseAuditEventRepository(dao);
}

If your document layout is different, implement MongoAuditEventDao and pass it to the repository.

Retention

The collection grows until you prune it. Use a TTL index on eventTime, or a scheduled delete:

db.audit_events.deleteMany({ eventTime: { $lt: cutoff } });

Redis

RedisAuditEventRepository persists events to Redis using Spring Data Redis.

Storage model

Events are stored in a Redis sorted set:

Scoring by timestamp gives time-ordered storage for free, and lets find(principal, after, type) push the after criterion down to Redis as a ZREVRANGEBYSCORE query. principal and type are then matched in memory. Because the whole event is stored as JSON and reconstructed on read, no information is lost.

Redis does have a dedicated time-series type, but only through the RedisTimeSeries module in Redis Stack, which Spring Data Redis does not wrap. The sorted set approach gives equivalent time-range querying using core Redis.

A sorted set member is unique by its value. Two events serializing to exactly the same JSON, meaning the same timestamp, principal, type and data, collapse into a single entry. In practice audit events differ, so this is rarely a concern, but it is a property of the storage model rather than something the repository can prevent.

Dependencies

spring-data-redis, an optional dependency of this library, plus a Redis client, where Lettuce is the Spring Boot default. The Spring Boot starter gives you both:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

You also need a configured Redis connection so that Spring Boot provides the StringRedisTemplate. See Spring Boot, Redis and the spring.data.redis.* properties.

Wiring it up

@Bean
AuditEventRepository auditEventRepository(final StringRedisTemplate redisTemplate) {
  final AuditEventMapper eventMapper = new JsonAuditEventMapper(JsonMapper.builder().build());
  return new RedisAuditEventRepository(redisTemplate, "audit:events", eventMapper);
}

audit:events is the key under which the sorted set is stored. Choose whatever suits your key naming, for example a per-application prefix.

Retention

Redis does not cap the sorted set. Either set a TTL on the whole key with redisTemplate.expire(...) if you only need a recent window, or trim by score periodically:

redisTemplate.opsForZSet().removeRangeByScore("audit:events", 0, cutoff.toEpochMilli());

Syslog

SyslogAuditEventRepository sends each event, serialized as JSON, as the message body of a syslog message.

Syslog is a write-only sink, so this repository does not support querying.

How syslog works in Java

There is no programmatic syslog sender in Spring or the JDK. The usual options are the logging frameworks’ syslog appenders, Logback’s or Log4j2’s SyslogAppender, or a small client library. This repository uses syslog-java-client, which supports RFC 3164 and RFC 5424 over UDP, TCP and TCP with TLS.

Dependency

syslog-java-client is an optional dependency of this library. Declare it in your application:

<dependency>
  <groupId>com.cloudbees</groupId>
  <artifactId>syslog-java-client</artifactId>
  <version>1.1.7</version>
</dependency>

Wiring it up

You configure a SyslogMessageSender, covering transport, host and port, facility, severity, message format and application name, and hand it to the repository. All syslog protocol concerns stay in the sender. The repository only writes the event as the message body.

@Bean
AuditEventRepository auditEventRepository() {
  final UdpSyslogMessageSender sender = new UdpSyslogMessageSender();
  sender.setSyslogServerHostname("logs.example.com");
  sender.setSyslogServerPort(514);
  sender.setDefaultAppName("my-service");
  sender.setDefaultFacility(Facility.LOCAL0);
  sender.setDefaultSeverity(Severity.INFORMATIONAL);
  sender.setMessageFormat(MessageFormat.RFC_5424);

  final AuditEventMapper eventMapper = new JsonAuditEventMapper(JsonMapper.builder().build());
  return new SyslogAuditEventRepository(sender, eventMapper);
}

For reliable, secure delivery use TcpSyslogMessageSender, optionally with SSL. The rest is identical.

The sender lifecycle is yours. SyslogMessageSender is Closeable. UDP is connectionless, but a TcpSyslogMessageSender holds a connection, so register it as @Bean(destroyMethod = "close").

Message size. UDP syslog messages may be truncated by intermediaries. Large audit events are better sent over TCP.

The delegating repository

DelegatingAuditEventRepository forwards events to several repositories at once, for example to persist to a database and also ship to syslog. This is what the auto-configuration builds when more than one backend is configured.

Delegate order matters

Since a query is answered by the first delegate returning a result, list the delegates in the order you want them consulted: the most complete store first, the most limited one last. In particular, an in-memory repository should always come last, for the reason given in Configuration.

Filtering

Configure the event filter on the delegating repository, not on the individual delegates. Filtering there is applied once, consistently, for every delegate, whereas per-delegate filters are easy to get out of sync.

Write failure handling

Every delegate is attempted even if an earlier one fails, and each failure is logged. Whether the overall add(...) then throws is resolved per delegate:

Delegate’s throwOnWriteFail Result on that delegate’s failure
Explicitly true Throw. The delegate’s choice wins.
Explicitly false Log only. The delegate’s choice wins.
Unset Inherit the delegating repository’s setThrowOnWriteFail(...) value, which itself defaults to throwing.

If, after all delegates have been attempted, any failed delegate’s resolved value was “throw”, the delegating repository throws an AuditEventWriteException. The first failure is thrown and the rest are attached as suppressed exceptions.

Set the policy once on the delegating repository, and override it on a specific delegate only when that delegate needs different behaviour, for example “the local file sink must never break the request, but a failure to write to the database should”.

Wiring it up

@Bean
AuditEventRepository auditEventRepository(final DatabaseAuditEventRepository database,
    final SyslogAuditEventRepository syslog) {

  final DelegatingAuditEventRepository repository =
      new DelegatingAuditEventRepository(List.of(database, syslog),
          AbstractAuditEventRepository.exclusionPredicate(List.of("noisy_event_type")));

  // Best effort: a failing sink should not break the audited operation.
  repository.setThrowOnWriteFail(false);
  return repository;
}

Because the filter is on the delegating repository, the two delegates above should be created without filters of their own.


Copyright © 2026, Myndigheten för digital förvaltning - Swedish Agency for Digital Government (DIGG). Licensed under version 2.0 of the Apache License.