spring-audit-support

Logo

Usage

License


This page describes the concepts of the library and how they fit together. See Configuration for the properties that set it all up, and Repositories for where the events end up.

From application event to audit entry

An application should not have to know that auditing exists. It publishes an ordinary Spring ApplicationEvent saying what happened, and the audit machinery decides whether that is worth auditing and what the audit entry should look like.

ApplicationEvent  ──▶  AuditApplicationListener  ──▶  EventTransformer  ──▶  AuditEvent  ──▶  AuditEventRepository

AuditApplicationListener listens to every application event published in the context. For each one it resolves what to do, in this order:

  1. An AuditApplicationEvent is ignored. It has already been processed.
  2. A PayloadApplicationEvent carrying an AuditEvent payload is published as an AuditApplicationEvent.
  3. An event whose source is an AuditEvent is published as an AuditApplicationEvent.
  4. An event that itself implements EventTransformer and supports itself is transformed and published.
  5. Otherwise, the first registered EventTransformer that supports the event is used.

If no transformer matches, the event is ignored. This is the normal case: most application events are not audit events.

Cases 2 and 3 exist because a bare AuditEvent published on its own does not reach the actuator’s AuditEventRepository. It has to be wrapped in an AuditApplicationEvent, and the listener does that wrapping for you.

Note that the listener uses the first transformer that supports an event. Registering two transformers that both support the same event type means one of them silently never runs. One transformer per event type.

Event transformers

An EventTransformer has two methods: supports(ApplicationEvent), which decides whether it handles a given event, and transform(ApplicationEvent, AuditEventContext), which produces the audit event.

Declare a transformer as a bean and the auto-configuration registers it with the listener.

@Bean
EventTransformer userLoginTransformer() {
  return new UserLoginEventTransformer();
}

For the common case of a transformer handling exactly one event class, use SingleEventTransformer, which implements supports from a declared event type, so only the transformation itself has to be written.

An event class may also implement EventTransformer itself, so that the event carries the knowledge of how it is audited. Such an event needs no registered bean at all, since the listener finds the transformer on the event. The library’s own SystemAlertEvent works this way, and is worth reading as a complete example.

The audit event context

Before a transformer is invoked, the listener asks its AuditEventContextResolver for an AuditEventContext. The context carries the ambient facts that every audit event needs and that no individual transformer should have to work out for itself: the application name, the application version, the correlation ID, the trace ID and the principal.

The default resolver takes the application name and version from configuration, the correlation ID and trace ID from the identifier storage, and the principal from the Spring Security context, falling back to a configured default principal when no user is authenticated.

The event being audited is passed to the resolver, so a custom resolver may base its result on it. See Configuration for how to supply your own.

The audit event

AuditEvent extends Spring Boot’s org.springframework.boot.actuate.audit.AuditEvent, so it is still a Spring audit event and still travels through the actuator’s infrastructure. What it adds is structure.

Every audit event has the same base fields:

Field Content
type The audit event type, the unique name identifying the kind of event.
timestamp The instant when the event occurred. The current time is used if none is supplied.
application The application that produced the event, holding a name and a version. Each member is omitted if not available, and the whole field is omitted if neither is.
correlation_id The correlation ID tying the event to a flow that may span several requests. Omitted if not available.
trace_id The trace ID tying the event to a single request, possibly handled by several services. Omitted if not available.
principal The initiator of the audited operation. For events not tied to an end user the system principal, system, is normally used. Omitted if not available.
data An object holding the event-specific content. Its members are defined by the individual event.

The two values of application describe one subject, the producing application, which is why they sit together in one member rather than as siblings at the root. The name matters when logs from several applications are shipped to the same log server, and the version tells which build of that application produced the entry - which matters when several versions run at once, or when an entry is read long after it was written. The version is optional, see Configuration. The correlation ID and the trace ID are what let entries be grouped, and they are two different things: a correlation ID may span many requests, a trace ID lives within one. See Correlation ID and Trace ID.

Fields that are empty are omitted from the serialized output, so an event carries only what is actually known.

A serialized event has this shape:

{
  "type": "user_login",
  "timestamp": "2026-07-31T09:12:44.001Z",
  "application": {
    "name": "my-service",
    "version": "1.2.3"
  },
  "correlation_id": "b1f2c3d4-8a91-4f0e-9c22-7b5d3e1a0f44",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "principal": "198501011234",
  "data": {
    "user_id": "198501011234",
    "display_name": "Alice Andersson"
  }
}

Additional fields may be placed at the root level rather than inside data, for the rare case where something belongs alongside the base fields. Use this sparingly. Root level is for facts about the event itself, data is for what the event is about.

Audit types

The event type is not a bare string but an AuditType, a small value type that rejects an empty value.

public static final AuditType USER_LOGIN = AuditType.of("user_login");

Declaring the types an application produces as constants in one place gives you the list of what your audit log can contain, which is the starting point for documenting it.

Audit values

In Spring’s audit event, the data is a Map<String, Object>. Anything can be put in it, under any name, at any call site. Two entries of the same type may therefore have different members, different names for the same thing, or values of different types. An audit log built that way cannot be documented reliably, and whoever consumes it, an operator, an analyst, a log pipeline, cannot depend on the format.

The library builds event data from AuditValue objects instead. An AuditValue is a named value of a known type. Because the value objects for an event type are created in one place, every entry of that type comes out with the same members and the same names, and the shape of the event can be written down and relied upon.

The value types

Type Holds
StringAuditValue A string
IntegerAuditValue An integer
BooleanAuditValue A boolean
InstantAuditValue An Instant
ListAuditValue A list of serializable values
MapAuditValue A nested object of named values

The simple types are created directly:

final StringAuditValue userId = new StringAuditValue("user_id", "198501011234");
final BooleanAuditValue signed = new BooleanAuditValue("signed", true);

MapAuditValue and ListAuditValue have builders, since they hold more than one thing:

final MapAuditValue authnInfo = MapAuditValue.builder()
    .name("authn_info")
    .value("method", "bankid")
    .value("level", "loa3")
    .build();

final ListAuditValue scopes = ListAuditValue.builder()
    .name("scopes")
    .value("openid", "profile")
    .build();

An audit value serializes to its name and value, so the two above become:

"authn_info": {
  "method": "bankid",
  "level": "loa3"
},
"scopes": [ "openid", "profile" ]

Names are written in snake case, which is the convention used throughout the serialized event.

Common values

AuditValueConstants supplies factory methods for values that recur across applications, so that the same concept gets the same name everywhere: userId, displayName, givenName, surname, email, personalIdentityNumber, and error.

AuditValueConstants.userId("198501011234");         // "user_id": "198501011234"
AuditValueConstants.personalIdentityNumber(pnr);    // "personal_identity_number": ...

error builds a MapAuditValue named error with the members code, message, exception_class and details:

AuditValueConstants.error("invalid_request", "Missing parameter", IllegalArgumentException.class, null);

Use these rather than rolling your own equivalents. Consistent naming across applications is most of the value of a structured audit log.

Building an audit event

AuditEventBuilder assembles the event. Inside a transformer, build it from the context, which initializes the application name and version, correlation ID, trace ID and principal for you:

public class UserLoginEventTransformer implements SingleEventTransformer<UserLoginEvent> {

  public static final AuditType USER_LOGIN = AuditType.of("user_login");

  @Override
  public AuditEvent transformEvent(final UserLoginEvent event, final AuditEventContext context) {
    return AuditEventBuilder.builder(context)
        .type(USER_LOGIN)
        .timestamp(event.getLoginTime())
        .principal(event.getUserId())
        .dataField(AuditValueConstants.userId(event.getUserId()))
        .dataField(AuditValueConstants.displayName(event.getDisplayName()))
        .dataField(MapAuditValue.builder()
            .name("authn_info")
            .value("method", event.getMethod())
            .value("level", event.getLevel())
            .build())
        .build();
  }

  @Override
  public Class<UserLoginEvent> getEventType() {
    return UserLoginEvent.class;
  }
}

Anything taken from the context may be overridden, as the principal is above. Without a call to timestamp(...) the current time is used.

AuditEvent may also be subclassed for an event that an application produces often, so that its structure is fixed in a type rather than reassembled at each call site.

System alerts

The library supplies one ready-made event. SystemAlertEvent is published by an application to raise an operational condition that operators should know about, typically an error or an anomaly.

publisher.publishEvent(new SystemAlertEvent("Failed to reach the signature service", exception));

It becomes an audit event of type system_alert with the system principal, carrying a data.alert_info object with message, exception_class and exception_message. Since the event implements EventTransformer itself, nothing needs to be registered.

For reporting errors, subclass AbstractErrorEvent. It takes an error code, and optionally a message, the class of the exception that caused the error, and further details, and places them in a data.error object. The subclass supplies the audit type and may add its own data fields. Like SystemAlertEvent, it transforms itself, so nothing needs to be registered.

The library also supplies two transformers for auditing the application lifecycle, ApplicationReadyEventTransformer and ContextClosedEventTransformer, which produce audit events of type system_started and system_shutdown. These are registered by the auto-configuration, so no application code is needed. Declare your own bean of either type to replace the supplied one, or assign audit.log-lifecycle-events to false to leave the application lifecycle unaudited.

All of the library’s own audit events, and their data, are documented under Audit Events.


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