Audit Logging¶
An audit trail is a permanent record of who did what, when, and whether it
worked — "user admin suspended tenant acme at 14:03, success". The kit records
this trail asynchronously, off the request's critical path, and persists each
event to PostgreSQL with its metadata as JSONB.
New here? Do the Tutorial first. This guide assumes you have a running app.
How it flows¶
your code kit PostgreSQL
───────── ─── ──────────
audit.publish(event) ──► publish() hands it to an ──► one row in the
async executor (off the audit table, metadata
request thread) stored as JSONB
Because the write is async, a slow or failing audit write never slows down — or fails — the request that triggered it.
Emit an event¶
Inject AuditEventPublisher and publish an AuditEvent. Build the event with
AuditEvent.builder():
// src/main/java/com/example/myapp/TenantAdminService.java
import java.time.Instant;
import java.util.Map;
import kr.devslab.kit.audit.AuditAction;
import kr.devslab.kit.audit.AuditEvent;
import kr.devslab.kit.audit.AuditEventPublisher;
import kr.devslab.kit.audit.AuditOutcome;
import kr.devslab.kit.audit.AuditTarget;
@Service
class TenantAdminService {
private final AuditEventPublisher audit;
TenantAdminService(AuditEventPublisher audit) { this.audit = audit; }
void suspend(String tenantId, String reason) {
// … perform the change …
audit.publish(AuditEvent.builder()
.action(AuditAction.of("tenant.suspend")) // required
.target(new AuditTarget("tenant", tenantId))
.outcome(AuditOutcome.SUCCESS) // SUCCESS | FAILURE
.occurredAt(Instant.now()) // required
.metadata(Map.of("reason", reason)) // free-form → JSONB
.build());
}
}
The fields¶
| Field | Required | What it is |
|---|---|---|
action |
yes | What happened, as a stable code: AuditAction.of("tenant.suspend"). |
occurredAt |
yes | When — Instant.now(). |
target |
no | What was acted on: new AuditTarget(type, id), e.g. ("tenant", "acme"). |
outcome |
no | AuditOutcome.SUCCESS or AuditOutcome.FAILURE. |
actor |
no | Who did it: new AuditActor(userId, tenantId, displayName). |
metadata |
no | Any extra context as a Map<String,Object> — stored as JSONB. |
ip / userAgent |
no | Request origin, when you have it. |
occurredAt is required, actor is not auto-filled
build() throws if occurredAt is missing — always set it. actor is optional,
but the kit does not fill it from the security context for you: if you want
the acting user recorded, set actor explicitly (e.g. from your CurrentUser).
Read the trail¶
Open the admin console → Audit Logs: a searchable, lazily-paginated table with filters (actor / action / target type / outcome / time range) and a JSON-payload detail drawer. See the Admin Console guide → Audit Logs.
# filter by action + outcome + time range:
curl -G localhost:8080/admin/api/v1/audit-logs \
-H 'Authorization: Bearer <token>' \
--data-urlencode 'tenantId=default' \
--data-urlencode 'action=tenant.suspend' \
--data-urlencode 'outcome=FAILURE' \
--data-urlencode 'from=2026-06-01T00:00:00Z'
Returns a paged list. See the Admin REST API for the
full audit-logs query parameters.
Tuning¶
| Key | Default | |
|---|---|---|
audit.enabled |
true |
Turn the whole subsystem off if you don't need it. |
audit.async-queue-capacity |
1024 |
Bounded queue feeding the async writer. |
The queue is bounded on purpose — under a flood, audit writes shed rather than exhaust memory. Size it for your throughput. See the Configuration reference.
See also¶
- Admin Console → Audit Logs — the searchable viewer.
- Admin REST API — the
audit-logsresource.