Keep rotated log files for a set number of days
ci / fork-checks (pull_request) Successful in 2m28s
ci / build (pull_request) Successful in 3m47s

Personal-data catalog spec, default D1 (settled 2026-09-28): log files
were never deleted. inbuxa:LogSettings.keepForDays says how many days
rotated log files are kept; unset (null) keeps every file, as before,
and a new install sets 30 days.

It is a fork-owned setting, stored under T + l as audit retention is,
not a field on x:TracerLog: that object is also stored inside
x:Bootstrap with a field after it, so a new field would change
x:Bootstrap's stored format. Server-level, with the tracers'
permissions (sysTracerGet, sysTracerUpdate); changes are in the audit
log, before and after.

Log files are local, so every node deletes its own: hourly, and at once
when the setting changes on that node. Only regular files named
<prefix>.<something> in each enabled log tracer's directory, last
changed more than the limit ago, are removed; the file being written is
never that old, and nothing else in the directory is touched. Minimum
one day. The catalog classifies inbuxa:LogSettings and points the log
file's retention at it.

Tested: unit tests for the file rule (only this log's old files; the
current file, other files and directories stay) and a purge on disk;
the system suite, which reads, sets, refuses zero, restores null and
checks the audit records; fork checks.
This commit is contained in:
2026-09-28 08:23:36 -07:00
parent bdd97c5828
commit 1d5a49409f
21 changed files with 789 additions and 2 deletions
+55
View File
@@ -236,6 +236,61 @@ pub async fn test(test: &mut TestServer) {
"the expired ban's record is gone"
);
// inbuxa: personal-data catalog, D1: how long rotated log files are kept.
// Unset keeps every file; a value is days; zero is refused; null goes back
let using = &["urn:ietf:params:jmap:core", "urn:inbuxa:jmap"];
let account = admin.id_string().to_string();
let log_get = || {
admin.jmap_request(
using,
json!([["inbuxa:LogSettings/get", {"accountId": account, "ids": null}, "0"]]),
)
};
let log_set = |keep: serde_json::Value| {
admin.jmap_request(
using,
json!([["inbuxa:LogSettings/set", {"accountId": account,
"update": {"singleton": {"keepForDays": keep}}}, "0"]]),
)
};
let got = log_get().await;
assert_eq!(
got.0.pointer("/methodResponses/0/1/list/0/keepForDays"),
Some(&serde_json::Value::Null),
"a server that never set it keeps every file: {}",
got.0
);
let set = log_set(json!(14)).await;
assert!(set.0.pointer("/methodResponses/0/1/updated/singleton").is_some(), "{}", set.0);
let got = log_get().await;
assert_eq!(got.0.pointer("/methodResponses/0/1/list/0/keepForDays"), Some(&json!(14)));
let refused = log_set(json!(0)).await;
assert_eq!(
refused.0.pointer("/methodResponses/0/1/notUpdated/singleton/type"),
Some(&json!("invalidProperties")),
"{}",
refused.0
);
log_set(serde_json::Value::Null).await;
let got = log_get().await;
assert_eq!(
got.0.pointer("/methodResponses/0/1/list/0/keepForDays"),
Some(&serde_json::Value::Null)
);
// Each change is in the audit log, before and after
let query = admin
.jmap_request(
using,
json!([["inbuxa:AuditEvent/query", {"accountId": account,
"filter": {"targetKind": "inbuxa:LogSettings"}}, "0"]]),
)
.await;
assert!(
query.0.pointer("/methodResponses/0/1/ids").and_then(|ids| ids.as_array()).is_some_and(|ids| ids.len() >= 2),
"log settings changes aren't recorded: {}",
query.0
);
// Make sure the IP remains unblocked after reload
admin.registry_create_object(Action::ReloadBlockedIps).await;
validate_password_with_ip(