
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.
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.
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.
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.
Two query paths exist:
find(principal, after, type), from Spring Boot’s own interface. Where the backend can, this is pushed down to
the store. null arguments are ignored.find(Predicate<AuditEvent>), an arbitrary predicate. This cannot be pushed down to any of the backends, so it
is evaluated in memory over the most recent events. The window defaults to 1000 and is set with setMaxFetch(int). A
warning is logged if the limit truncates the result.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 |
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.
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.
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.
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>.lcklock file next to it. Use a singleFileBasedAuditEventRepositoryinstance per file within the JVM.
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.
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.
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.
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
);
@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.
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.
The same DatabaseAuditEventRepository as above, with DefaultMongoAuditEventDao as the DAO. It uses Spring Data
MongoDB’s MongoOperations, typically a MongoTemplate.
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 });
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.
@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.
The collection grows until you prune it. Use a
TTL index on eventTime, or a scheduled delete:
db.audit_events.deleteMany({ eventTime: { $lt: cutoff } });
RedisAuditEventRepository
persists events to Redis using Spring Data Redis.
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.
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.
@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.
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());
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.
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.
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>
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.
SyslogMessageSenderisCloseable. UDP is connectionless, but aTcpSyslogMessageSenderholds 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.
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.
add(event) applies this repository’s own filter and forwards accepted events to all delegates.find(principal, after, type) and find(criteria) both try each delegate in order and return the first
non-empty result. Results are never merged. They differ in which delegates they can use: find(principal, after,
type) uses all delegates, since every AuditEventRepository offers that method, while find(criteria) only uses
delegates that are ExtendedAuditEventRepository instances supporting find.supportsFind() is true if at least one delegate is an ExtendedAuditEventRepository whose supportsFind()
is true, so it reports whether a predicate based query can be served. When it is false, find(criteria) returns
an empty list. A plain AuditEventRepository delegate does not make it true, even though such a delegate can still
answer find(principal, after, type).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.
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.
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”.
@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.