審計事件
概述
本頁描述已接受 RFC 的實作,目標 4.0.0-alpha.33,已於2026-10-11通過四輪人審,仍待發布;FU-136 的發布前 MySQL 健康路徑已另行驗證。
已發布的 PersistentAuditEventRepository 保留同步 JPA 契約;更新 feature 接線才會採用新預設。
AuditEvent 保存登入、授權、acting 存取等操作事件。新的 audit feature 使用共用
AsyncAuditEventRepository:呼叫者建立有大小上限的 JSON 快照,以非阻塞 admission
交給有界佇列;單一背景工作者直接以 JDBC 批次寫入應用提供的資料庫。
寫入不加入業務交易,寫入、編碼或診斷的普通失敗不會送回原呼叫者。
事件 payload 應使用已解析的值/DTO,不能依賴 lazy Entity、具副作用的 getter 或呼叫者
thread-local。JSON 編碼發生於呼叫端,無法替任意 getter 的 I/O、阻塞或交易副作用提供隔離。
這是可遺失的最佳努力紀錄:關閉、容量不足、admission gate 暫時忙碌、快照錯誤、寫入失敗、停止及程序崩潰都可能丟失事件。 沒有重試、outbox、同步 fallback 或可靠遞送保證。它也不證明原業務交易已提交;業務回滾後, 已接受的審計事件仍可能落庫。Entity revision 與 JPA auditing 的交易語意不受此開關控制。
第一階段以穩定釋出與交易隔離為優先。add() 不等待 admission gate;持鎖執行緒即使只做
短操作,也可能被排程暫停。競爭時以 BUSY 計數略過,不能把佇列未滿解讀為每筆都保證接受。
維運有權停用整個 AuditEvent 入口;實際開關切換另嘗試由背景工作者記錄操作者與前後狀態,
即使事件紀錄關閉也會嘗試,但不保證保存。
acting 事件發布 hook 仍保留,但停用期間及遺失時不保證其存取軌跡。需要完整、防竄改或可靠
稽核的應用,須另行設計;本階段不新增可靠遞送或等待鎖的事件寫入路徑。
快速開始
安裝 audit feature 後預設啟用,並使用應用唯一的預設 DataSource。參考實作沿用既有
audit_event 的五欄:UUID 字串 id、最多100字元的 principal/event_type、
JSON 文字 event_data、UTC event_at。表名、欄位與 schema 都由應用擁有;框架不建立資料表。
超過100字元的 principal 轉為固定 SHA-256 摘要,查詢採相同正規化。
非 scaffold 消費者可自行組裝;由 Spring 管理以下 bean 才會自動啟停工作者:
import io.leandev.appfuse.audit.AsyncAuditEventRepository;
import io.leandev.appfuse.audit.AuditJdbcMapping;
import io.leandev.appfuse.audit.AuditWriteSettings;
import io.leandev.appfuse.audit.JdbcAuditStorage;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
@Bean
AsyncAuditEventRepository auditEventRepository(DataSource dataSource) {
var settings = AuditWriteSettings.defaults();
var mapping = new AuditJdbcMapping("audit_event", "id", "principal",
"event_type", "event_data", "event_at");
return new AsyncAuditEventRepository(new JdbcAuditStorage(dataSource, mapping, settings), settings);
}
資料庫暫時離線或表不可寫不由 adapter 在啟動時探測。應用其他 JPA/Flyway 初始化仍可能 需要資料庫,這項隔離不改變那些初始化要求。無效容量、多個 provider、DataSource 選擇不明、 不支援的 pool 所有者或 transaction-aware proxy 則在啟動時明確報錯。
沒有 storage provider 時,預設五欄映射仍由應用 AuditConfig 宣告,讓最簡單的接線沿用
既有資料庫。reference domain 的 AuditPersistenceConfig 展示相同映射的顯式 provider,供
下游替換儲存位置;兩者選一,不會各自建立 writer。移除 reference domain 不等於建立資料表:
下游須保留/自行管理 audit_event,或提供其他 mapping/storage。
缺表不在啟動時探測範圍;維運以 auditcontrol 的 BATCH_FAILED 增長及 WRITTEN 未增長辨識
寫入異常,並由資料庫管理工具確認 schema。普通寫入失敗仍可保持 RUNNING,不能只看 state
或 enabled 判定資料已落庫;同步 find() 的 QUERY_FAILED 也不偽裝成空結果。
功能說明
開關與查詢
AuditControl.status() 回傳 enabled、accepting、state、queued、inFlight、quarantined 與固定計數。
setEnabled(false) 在短 gate 內切換 generation 並清空待寫事件;關閉後的新事件不建立快照、
不寫資料庫,也不改存記憶體。關閉前開始建立、尚未入列的舊快照,在重新啟用後也會被略過。
工作者在同一 gate 內取出固定批次並標記 inFlight,這是唯一的「開始寫入」邊界。
若已標記,即使尚未取得 Connection,關閉後也允許該批次完成或失敗;不強制中斷資料庫交易。
維運 HTTP 入口為 GET /actuator/auditcontrol 與 POST /actuator/auditcontrol,
POST body 為 {"enabled":false} 或 {"enabled":true}。
GET/HEAD 需要應用集中式 actuator:read,POST 需要 actuator:write,範圍是本實例。
端點隨 management base path 與 endpoint mapping 移動;不新增 audit 專屬 authority。
執行中值不回寫設定,重啟沿用 app.audit.enabled。不要以 management.auditevents.enabled
替代此開關:若另外關閉 Boot auto-audit,runtime 啟用不會重建那些 listener。
開關操作紀錄
實際 enabled 值改變時,控制介面嘗試將時間、操作者、舊值與新值加入獨立有界 queue,
由既有單一工作者輸出 Audit control INFO 應用日誌;不寫入 audit_event。
POST 從已驗證的請求以 ActingContext.current().getActor() 取得真實操作者 principal。
模擬時(isImpersonating())另記有效身份 getSubject():A 模擬 B 切換開關會記
principal="A" subject="B",不將 B 當成操作者;純代理時 actor 為登入者,不另記 subject。
程式呼叫可用 setEnabled(boolean, String) 傳入真實操作者,或相容三參數 overload
setEnabled(boolean, String, String) 另傳模擬時的 subject(非模擬傳 null)。
原單參數介面使用 (unspecified),既有控制器的 default overload 保留原切換行為。
同值呼叫、FAILED/STOPPING/CLOSED 拒絕的切換不產生成功紀錄。初始設定及停止不冒充人工切換。
操作 queue 和事件 queue 各自以 queue-capacity 為上限,另可各有一筆操作/一個資料庫批次處理中。 關閉事件入口不清空操作 queue。工作者每輪最多處理 batch-size 筆操作,logger 在 gate 外執行。 停止/工作者終止會丟棄待記操作,已開始的 logger 呼叫可能於 close 返回後結束;不強制中斷 appender。 操作紀錄不等待資料庫或日誌完成;沿用開關本身的短 gate 協調,不新增記錄完成等待。 共用工作者若卡在資料庫或 appender,兩個 queue 均可能滿而遺失,沒有 caller-runs/重試/同步 fallback。
principal 及可選 subject 各最多取前100字元,超長加 [truncated];控制字元、行分隔、引號及反斜線換成 ?。
這是有界、安全的診斷識別,可能截斷或碰撞,不是完整身分憑證;不帶事件 payload、token 或原始例外。
logger 拋 RuntimeException 時增加 CONTROL_LOGGER_FAILED,停止再次嘗試操作 logger;後續切換仍生效。
CONTROL_ACCEPTED、CONTROL_LOGGED、CONTROL_DROPPED 可由 status 讀取;LOGGED 只表示 logger 呼叫返回,
logger 級別過濾或 appender 內部丟棄不一定可偵測,不能作為已保存證據。正常 ACCEPTED/LOGGED 不觸發 WARN。
日誌權限、保留期及輸出路由由應用擁有;操作紀錄同樣以不影響交易為優先,不能證明停用期間的完整操作軌跡。
AuditEventRepository.find(principal, after, type) 同步查詢已持久化資料,按時間排序;
null 表示無該篩選,after 為排他邊界。它不等待佇列排空;查詢失敗向呼叫者報錯,不能視為空結果。
add() 的失敗隔離承諾不延伸為同步查詢的失敗隔離。
停止與故障
Spring lifecycle 或 close() 停止時清空 pending,最多等待 shutdown-wait;殘留 daemon
僅可結束已開始的批次或 lease 清理,不能開始新批次、重新啟用,也不能關閉應用 DataSource。
STOPPING/CLOSED 都是終止狀態。
repository 的 isPauseable() 回傳 false,避免 Spring 7 的 test-context cache 暫停把不可重啟
的 writer 終止;實際 stop/context close 仍執行終止流程。
普通 batch 失敗在確認 rollback、statement 清理與狀態還原後才可歸還連線。
commit 結果未知、rollback/statement cleanup/reset 失敗會停止 writer,進入 FAILED,
先使用所屬 pool 的淘汰能力移除 lease。處置無法確認時保留一個隔離 lease,
回報 quarantined;絕不以一般 close 歸還可能污染的連線,亦不關閉共享池。
FAILED 不能靠開關恢復,需維運修正並重啟。
配置選項
| 設定 | 預設 | 意義 |
|---|---|---|
app.audit.enabled | true | 啟動時是否接受事件 |
app.audit.queue-capacity | 1000 | 事件及操作 queue 各自的 pending 上限 |
app.audit.batch-size | 100 | 不得大於 queue-capacity |
app.audit.worker-count | 1 | 第一階段只接受1 |
app.audit.flush-interval | 1s | 未滿批次的最長排程等待,不是落庫 SLA |
app.audit.max-payload-bytes | 65536 | 編碼過程中的 UTF-8 JSON 上限 |
app.audit.statement-timeout | 3s | JDBC statement timeout;不足1秒向上取整 |
app.audit.shutdown-wait | 5s | 停止時等待工作者的上限 |
Connector/J 的 client-prepared statements 預設可能不提供 parameter metadata;adapter 對已知 不支援狀態使用 UTC TIMESTAMP 綁定,其他 metadata 錯誤仍走失敗處理。帶時區欄位則使用 JDBC 4.2 OffsetDateTime,避免環境時區改變 Instant。MySQL 8.4/DATETIME(6) 的真實驗證 由 JdbcAuditStorageMySqlIT 提供,需 Docker;缺少環境時明確跳過,不能算成已驗證的結果。
本輪實際驗證包含 H2 plain/zoned 欄位、Hikari 受控故障處置,及獨立執行的真實 MySQL 8.4.0 健康路徑:Connector/J 9.5.0、HikariCP 7.0.2、JDK 25.0.3,JVM 時區 Asia/Taipei; 預設 client-prepared statements 的批次、UTC 微秒、中文 JSON、UUID、排序與排他 after 均通過。 Docker IT 仍跳過,獨立證據見 RFC 發布前驗證。 Hibernate 歷史資料互通、MySQL 故障注入、PostgreSQL、SQL Server 或 Oracle 新寫入路徑仍未驗證, 由 FU-136 追蹤;應用已有 driver 設定不等於新 adapter 已完成認證, 不因未查明的 SQLSTATE 任意擴大 metadata fallback 白名單。
statement timeout 不涵蓋連線池取得或 commit,也不是可強制中斷任意 driver 的保證。 共用池仍可能產生連線、資料庫 CPU/I/O 與 JVM 資源競爭;業務端仍有快照 CPU 成本。 有界佇列與 payload 上限控制資源,不宣稱效能零影響或已完成正式負載測試。
進階用法
替換 adapter
應用的 AuditStorageProvider 現在回傳 AuditStorage,由 feature 包成唯一的共用 repository。
自訂 adapter 實作 writeBatch(List<AuditEventSnapshot>) 與 find(...);寫入只在私有工作者執行。
不能以 provider 回傳自己的 repository 繞過開關或 queue。
既有 AuditEventRepositoryProvider 需遷移到 AuditStorageProvider;參考實作改用
JdbcAuditStorage,既有 Entity/JPA repository 可保留供讀取與資料表管理。
PersistentAuditEventRepository、JPA base 與 converter 保留給顯式採用舊同步契約的應用,
不作新 feature 的 fallback。
預設連線處置以 HikariCP 7.0.2 的原始 borrowed proxy/所屬 pool 驗證;
僅自動辨識未包裝、未繼承的 HikariDataSource。
其他 pool 或 wrapped source 必須使用四參數 JdbcAuditStorage 提供相符的
AuditConnectionDisposer;正常返回代表已確認該 lease 不會再被池借出,不是只呼叫 close/abort。
不確定時必須拋錯,讓 adapter 隔離 lease。自訂 storage 同樣應以
AuditStorageException 的 terminal/quarantined 回報未知交易結果。
安全診斷
事件入口只更新固定計數,不呼叫 logger,也不保存原始例外供背景列印。背景診斷只含狀態 與計數;另有上節明列的操作 INFO 日誌。INFO/WARN 共用同一 logger;任一路徑拋 RuntimeException 後停止這兩條日誌路徑,不遞迴嘗試故障 logger,計數仍可由控制端點讀取。 阻塞的 appender 可停住工作者,造成 queue 滿而遺失,但不會轉回 caller 寫入或記錄。 DISABLED 與 CLEARED 是正常控制計數,持續停用/刻意清空不觸發週期 WARN;其他故障分類仍告警。
已驗證 Spring JDBC 7.0.2、HikariCP 7.0.2、H2 2.4.240 預設不啟用 trace 的組合。
JDBC adapter 直接擁有 lease,沒有 TransactionTemplate callback throwable logging。
reference config/feature-audit.yml 必須保留以下三個 logger 設定,避免 Hikari 把原始
SQLException/driver 訊息(可能含 payload/SQL 值)輸出。不關閉整個 package,保留
ProxyLeakTask 的洩漏偵測;已以真正借出未歸還的 Hikari lease 驗證 WARN 仍可輸出。
logging:
level:
com.zaxxer.hikari.pool.ProxyConnection: "OFF"
com.zaxxer.hikari.pool.PoolBase: "OFF"
com.zaxxer.hikari.pool.HikariPool: "OFF"
| HikariCP 7.0.2 logger | 查核路徑與處置理由 |
|---|---|
| ProxyConnection | checkException 將原始 SQLException 交給 WARN;連線損壞可能洩漏 batch 參數。保留 OFF。 |
| PoolBase | 驗證、建連、設定與 physical close 失敗輸出 driver 訊息/throwable。保留 OFF。 |
| HikariPool | 取連線、begin/end request 與 housekeeping 異常可能輸出原始訊息/throwable。保留 OFF;同 logger 的 starvation 診斷也會被抑制。 |
| ProxyLeakTask/PoolEntry | 洩漏借用堆疊及排程取消提醒不含事件值,維持應用原有層級。仍可能含連線 toString 與執行緒名;不要在這些名稱放敏感值。 |
這三個 OFF 仍影響共享業務池的診斷;維運須另看安全計數及既有 pool metrics。 不新增能可靠識別任意 driver 訊息的過濾器;不同 driver 的日誌行為尚待 FU-136 驗證。
外部設定若覆寫此 logger,便不在上述日誌安全驗證範圍。自訂 driver logging、SQL bind TRACE、 應用自行記錄原始事件、任意第三方 appender 與 JVM 致命 Error 不在保證範圍。
API 參考
公開入口為 AsyncAuditEventRepository、AuditControl/AuditStatus、AuditWriteSettings;
adapter 邊界為 AuditStorage、AuditEventSnapshot、JdbcAuditStorage、AuditJdbcMapping、
AuditConnectionDisposer 與只含安全分類的 AuditStorageException。
相關說明:Entity 歷程稽核、acting 稽核、 已接受的 RFC。