Read and manage the audit trail
Goal
For platform teams. In this task you read who did what in the control plane. You set how long events are kept. You also remove a person from the trail when someone asks you to. The audit trail is the list of the actions that people and the control plane made.
Before you start
- You have the CLI, and you are signed in. Sign in and get access shows how.
- To read every event, to filter by user and to erase a person, you need the role
operator. A person with the roleowner:<team>reads only the events of the applications of that team. - For an erasure you need a second operator. An operator cannot erase their own ID.
Steps
Know what is recorded
The control plane records these actions. Each action has its own name:
| Action | When it is recorded |
|---|---|
provisioner.registered | A provisioner is registered. |
provisioner.updated | A registration is changed. |
provisioner.deregistered | A registration is removed. |
application.registered | An application is registered. |
apply.requested | Someone asks for an apply. The event is recorded when the apply starts, and also when the control plane refuses it because another apply holds the lock. |
run.abandoned | An operator or the control plane releases a run. |
audit.subject_erased | An operator erases a user from the trail. |
A plan, a validate and every read are not recorded.
Each event holds these facts:
| Field | What it holds |
|---|---|
occurredAt | The time of the action. |
action | The name of the action. |
outcome | performed, or refused with a reason such as apply_in_progress. |
actorKind | The account type: person, service or system. |
actorSubject | The ID of the user, as the identity provider gives it. It is never a name. A system event has none. |
authority | The role that the user acted as, for example operator or owner:finance-team. It is system for an event of the control plane itself. |
targetType and targetId | What the action was about: a provisioner, a run, an application or an erasure. |
applicationId, environment, runId | The application, the environment and the run, when the action has them. |
traceId | The trace of the call, when the control plane sends traces. |
An event is written in the same step as its action. If the control plane cannot write the event, it does not do the action, and it answers 503.
Know the events of the control plane
The control plane writes some events itself. For example, it releases a run that wrote no event for a long time. Such an event has the account type system, the role system and no user ID. The CLI prints a dash in the column of the user ID. The JSON answer has no actorSubject:
TIME ACTION OUTCOME KIND SUBJECT AUTHORITY TARGET
2026-10-28T21:21:24.131Z run.abandoned performed system - system run:401e08b9-db9e-40e4-b42f-8844df431b31
{
"items": [
{
"id": "677f492a-5e45-48fa-8c00-2483ddb3e899",
"occurredAt": "2026-10-28T21:21:24.131Z",
"actorKind": "system",
"authority": "system",
"action": "run.abandoned",
"outcome": "performed",
"targetType": "run",
"targetId": "401e08b9-db9e-40e4-b42f-8844df431b31",
"applicationId": "0008a264-25c7-4502-94bf-e237fe92e034",
"environment": "test",
"runId": "401e08b9-db9e-40e4-b42f-8844df431b31"
}
],
"nextCursor": null
}
An owner sees these events too, because the run belongs to an application of the owner.
Read the trail with the CLI
joomiz audit prints the newest events first. The default is 50 events, and --limit takes a number from 1 to 1000:
joomiz audit --from 2026-10-28T19:18:00Z --limit 8
TIME ACTION OUTCOME KIND SUBJECT AUTHORITY TARGET
2026-10-28T19:20:16.016Z run.abandoned performed person 5d0f2c6e-9a41-4e1b-8c37-2b7f6a1d9e03 operator run:74c4e9a7-beb9-4557-bcdb-c5ccc2d730f4
2026-10-28T19:20:15.006Z apply.requested refused (apply_in_progress) person e2b84d17-6c90-4a35-9f1e-0d3a58b7c641 owner:finance-team application:dc891440-ffd2-4471-b5a8-24fc5214b9bf
2026-10-28T19:20:14.490Z apply.requested performed person e2b84d17-6c90-4a35-9f1e-0d3a58b7c641 owner:finance-team application:dc891440-ffd2-4471-b5a8-24fc5214b9bf
2026-10-28T19:19:00.680Z provisioner.updated performed person 5d0f2c6e-9a41-4e1b-8c37-2b7f6a1d9e03 operator provisioner:f690396e-d272-454e-886a-2f3e8e8cb458
2026-10-28T19:19:00.498Z application.registered performed person e2b84d17-6c90-4a35-9f1e-0d3a58b7c641 owner:finance-team application:dc891440-ffd2-4471-b5a8-24fc5214b9bf
The columns are the time, the action, the outcome, the account type, the user ID, the role and the target. The filters are:
| Option | What it keeps |
|---|---|
--action=<name> | One action, for example run.abandoned. |
--application=<id> | Events of one application. |
--run=<id> | Events of one run. |
--outcome=<outcome> | performed or refused. |
--from=<time> and --to=<time> | Events at or after the first time, and before the second. Use an ISO 8601 time. |
--actor=<subject> | Events of one user ID. Only an operator may use it. |
This shows who released a run:
joomiz audit --from 2026-10-28T19:18:00Z --action run.abandoned
TIME ACTION OUTCOME KIND SUBJECT AUTHORITY TARGET
2026-10-28T19:20:16.016Z run.abandoned performed person 5d0f2c6e-9a41-4e1b-8c37-2b7f6a1d9e03 operator run:74c4e9a7-beb9-4557-bcdb-c5ccc2d730f4
This shows the events of one run. Here the apply of a team, and the release by an operator:
joomiz audit --run 74c4e9a7-beb9-4557-bcdb-c5ccc2d730f4
TIME ACTION OUTCOME KIND SUBJECT AUTHORITY TARGET
2026-10-28T19:20:16.016Z run.abandoned performed person 5d0f2c6e-9a41-4e1b-8c37-2b7f6a1d9e03 operator run:74c4e9a7-beb9-4557-bcdb-c5ccc2d730f4
2026-10-28T19:20:14.490Z apply.requested performed person e2b84d17-6c90-4a35-9f1e-0d3a58b7c641 owner:finance-team application:dc891440-ffd2-4471-b5a8-24fc5214b9bf
This shows the refused applies:
TIME ACTION OUTCOME KIND SUBJECT AUTHORITY TARGET
2026-10-28T19:20:15.006Z apply.requested refused (apply_in_progress) person e2b84d17-6c90-4a35-9f1e-0d3a58b7c641 owner:finance-team application:dc891440-ffd2-4471-b5a8-24fc5214b9bf
Read the trail in the dashboard
The page Audit of the Platform dashboard shows the same events, newest first. The columns are Time, Action, Outcome, Authority, Actor, Subject and Target. The filters are the action, the outcome, the application ID, the run ID, and the times From and To. The button Load more events shows the next page. The filters stay in the address of the page, so you can share a search. Use the Platform dashboard describes the other pages.
The column Subject shows the user ID as plain text, (erased) after an erasure, and a dash for an event of the control plane itself. A person with the role owner:<team> also sees a dash for the event of an operator.
See what an owner sees
A person with the role owner:<team> reads the events of the applications of that team. The ID of an operator is hidden from that person. The CLI shows (hidden) in the column:
TIME ACTION OUTCOME KIND SUBJECT AUTHORITY TARGET
2026-10-28T19:20:16.016Z run.abandoned performed person (hidden) operator run:74c4e9a7-beb9-4557-bcdb-c5ccc2d730f4
2026-10-28T19:20:15.006Z apply.requested refused (apply_in_progress) person e2b84d17-6c90-4a35-9f1e-0d3a58b7c641 owner:finance-team application:dc891440-ffd2-4471-b5a8-24fc5214b9bf
2026-10-28T19:20:14.490Z apply.requested performed person e2b84d17-6c90-4a35-9f1e-0d3a58b7c641 owner:finance-team application:dc891440-ffd2-4471-b5a8-24fc5214b9bf
2026-10-28T19:19:00.498Z application.registered performed person e2b84d17-6c90-4a35-9f1e-0d3a58b7c641 owner:finance-team application:dc891440-ffd2-4471-b5a8-24fc5214b9bf
An owner cannot filter by user ID. The control plane answers 403 for --actor.
Read the trail with HTTP
The call is GET /audit-events. The query names are in snake_case: application_id, run_id, action, outcome, actor_subject, from, to, cursor and page_size. The page size is from 1 to 100, and the default is 50. When more events exist, the answer has a nextCursor. Send it as cursor for the next page. The control plane sends each answer on one line. The examples here have line breaks, so that you can read them. The field actorSubject is missing for an event of the control plane itself and for the event of an operator that an owner reads. For an erased event it is missing too, and the field actorErased is true. Control plane API reference shows the call with a token that is not on the command line.
curl -s -w '\nHTTP %{http_code}\n' --variable '%JOOMIZ_TOKEN' --expand-header 'Authorization: Bearer {{JOOMIZ_TOKEN}}' "$JOOMIZ_SERVER_URL/audit-events?from=2026-10-28T19:18:00Z&action=run.abandoned&page_size=1"
{
"items": [
{
"id": "e64214df-2097-47e1-b0dd-003a31b9f86d",
"occurredAt": "2026-10-28T19:20:16.016Z",
"actorKind": "person",
"actorSubject": "5d0f2c6e-9a41-4e1b-8c37-2b7f6a1d9e03",
"authority": "operator",
"action": "run.abandoned",
"outcome": "performed",
"targetType": "run",
"targetId": "74c4e9a7-beb9-4557-bcdb-c5ccc2d730f4",
"applicationId": "dc891440-ffd2-4471-b5a8-24fc5214b9bf",
"environment": "test",
"runId": "74c4e9a7-beb9-4557-bcdb-c5ccc2d730f4"
}
],
"nextCursor": null
}
HTTP 200
A bad value answers 400 with a text that names the rule:
{"detail":"audit query is invalid: outcome must be one of performed, refused","status":400,"title":"Invalid audit query"}
HTTP 400
Set how long events are kept
The control plane deletes events that are older than joomiz.audit.retention. The value is from 90 to 3650 days. The default is 400 days. The environment variable is JOOMIZ_AUDIT_RETENTION, for example 400d. A value outside the range stops the start of the control plane, and the error names the setting. A job runs every hour and deletes the old events in small steps. Settings reference lists the settings of the job.
Erase a person from the trail
A person can ask to be removed from the records. You erase the user ID of that person. The events stay, with their action, their target, their role and their time. Only the ID is removed, and the events show (erased) in its place.
- Get the user ID of the person from your identity provider. Write the request and the date in your own records. Joomiz does not keep the erased ID.
- Find the events of that ID. This shows what the erasure will change:
joomiz audit --from 2026-10-28T19:18:00Z --actor 5d0f2c6e-9a41-4e1b-8c37-2b7f6a1d9e03
TIME ACTION OUTCOME KIND SUBJECT AUTHORITY TARGET
2026-10-28T19:20:16.016Z run.abandoned performed person 5d0f2c6e-9a41-4e1b-8c37-2b7f6a1d9e03 operator run:74c4e9a7-beb9-4557-bcdb-c5ccc2d730f4
2026-10-28T19:19:00.680Z provisioner.updated performed person 5d0f2c6e-9a41-4e1b-8c37-2b7f6a1d9e03 operator provisioner:f690396e-d272-454e-886a-2f3e8e8cb458
- Ask another operator to send the erasure. The body has exactly one field,
subject:
curl -s -w '\nHTTP %{http_code}\n' --variable '%JOOMIZ_TOKEN' --expand-header 'Authorization: Bearer {{JOOMIZ_TOKEN}}' -X POST -H 'Content-Type: application/json' -d '{"subject": "5d0f2c6e-9a41-4e1b-8c37-2b7f6a1d9e03"}' "$JOOMIZ_SERVER_URL/audit-erasures"
{
"id": "c0799010-9fba-4c48-8415-6637281cb15b",
"erasedEvents": 2
}
HTTP 201
The answer is 201 with the ID of the erasure and the number of events that it erased. If the call stops before it ends, send the same call again. It finishes the work. A call that finds nothing left answers 201 with "erasedEvents": 0:
{
"id": "8e6b966f-308d-4c53-a68b-68b5b66248fe",
"erasedEvents": 0
}
HTTP 201
- Check the result. The events show
(erased), and the filter finds nothing:
TIME ACTION OUTCOME KIND SUBJECT AUTHORITY TARGET
2026-10-28T19:20:34.554Z audit.subject_erased performed person c19a7e44-3b52-4f08-a6d1-7e0b9c53f2a8 operator audit_events:8e6b966f-308d-4c53-a68b-68b5b66248fe
2026-10-28T19:20:34.536Z audit.subject_erased performed person c19a7e44-3b52-4f08-a6d1-7e0b9c53f2a8 operator audit_events:c0799010-9fba-4c48-8415-6637281cb15b
2026-10-28T19:20:16.016Z run.abandoned performed person (erased) operator run:74c4e9a7-beb9-4557-bcdb-c5ccc2d730f4
2026-10-28T19:20:15.006Z apply.requested refused (apply_in_progress) person e2b84d17-6c90-4a35-9f1e-0d3a58b7c641 owner:finance-team application:dc891440-ffd2-4471-b5a8-24fc5214b9bf
2026-10-28T19:20:14.490Z apply.requested performed person e2b84d17-6c90-4a35-9f1e-0d3a58b7c641 owner:finance-team application:dc891440-ffd2-4471-b5a8-24fc5214b9bf
2026-10-28T19:19:00.680Z provisioner.updated performed person (erased) operator provisioner:f690396e-d272-454e-886a-2f3e8e8cb458
2026-10-28T19:19:00.498Z application.registered performed person e2b84d17-6c90-4a35-9f1e-0d3a58b7c641 owner:finance-team application:dc891440-ffd2-4471-b5a8-24fc5214b9bf
no audit events found
The erasure is itself recorded, with the ID of the operator who sent it. That ID stays until the retention job deletes the event:
TIME ACTION OUTCOME KIND SUBJECT AUTHORITY TARGET
2026-10-28T19:20:34.554Z audit.subject_erased performed person c19a7e44-3b52-4f08-a6d1-7e0b9c53f2a8 operator audit_events:8e6b966f-308d-4c53-a68b-68b5b66248fe
2026-10-28T19:20:34.536Z audit.subject_erased performed person c19a7e44-3b52-4f08-a6d1-7e0b9c53f2a8 operator audit_events:c0799010-9fba-4c48-8415-6637281cb15b
After a restore from a backup
A restore from a backup brings back the events as they were on the day of the backup. The erased IDs return with them. Send the erasure requests again that you received after the date of the backup. Use your records of the requests, because Joomiz does not keep the erased ID.
Check
joomiz auditas an operator prints events, and as an owner it prints fewer and hides the IDs of operators.- After an erasure,
joomiz audit --actor <id>printsno audit events found. - The setting
joomiz.audit.retentionis in your configuration, and it is the value that you decided.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
403 and Operator role required for --actor or for an erasure | You have only the role owner:<team>. | Sign in as an operator. |
403 and an operator cannot erase their own subject | The ID in the body is your own. | Ask another operator to send the erasure. |
400 and Invalid audit erasure | The body is not exactly {"subject": "..."}, or the ID is empty or too long. | Send one field subject with 1 to 255 printable characters. |
400 and Invalid audit query | A filter has a bad value. | Read the text. It names the rule. |
503 and audit-unavailable | The control plane could not write the audit event. | Try again later. The action was not done. |
--limit must be a number from 1 to 1000 | The number is outside the range. | Use a smaller number. |
The control plane does not start and names joomiz.audit.retention | The value is below 90 days or above 3650 days. | Set a value in the range. |
Next
Check that the control plane is healthy in Check health and follow a run.