Audit logging

Read this page in the documentation

Audit logging An audit log answers "who changed this row, when, and what did it look like before?" — a different question from query logging, which only records the statements that ran. Prorm ships two implementations: | AuditLogger | @Audit decorator | --- | --- | --- | Where entries go | a database table | an in-memory array | Survives a restart | yes | no | Scope | every model on the connection, or chosen ones | one model class | Use for | compliance, forensics, change history | tests, local debugging | For regulated workloads that need tamper-evidence, retention policy and DSAR support, see Compliance — AuditTrail, WORMStorage and ImmutableRecord build on the same idea with stronger guarantees. AuditLogger setupHooks() walks prorm.getModels() and attaches hooks to each. Models defined after that call need audit.addHooksToModel(Model); hasHooks(Model) tells you whether one is already covered, and a model is never hooked twice. The audit table The first write defines and creates the model if it does not exist: The model is defined with timestamps: false and underscored: true, and recordId / userId are strings so any primary-key type fits. ipAddress is 45 characters — enough for an IPv6 address. What gets captured Hook | action | oldValues | newValues | --- | --- | --- | --- | beforeCreate | CREATE | — | the new row | beforeUpdate | UPDATE | the previous values | the new row | beforeDestroy | DELETE | the row as it stood | — | Entries are written from before hooks, so an audit row exists even if the write then fails — you get a record of the attempt. Values come from instance.toJSON(), which means hidden attributes stay hidden and virtual fields are included. Because capture is hook-based, it sees instance writes. Static bulk statements (Model.update({…}, { where }), Model.destroy({ where }), bulkCreate without individualHooks) do not construct instances and so are not captured. Route audited changes through instances, or call logAction() yourself. Reading the history Omit recordId for the whole table's history. The default page is 100 entries, newest first. Logging by hand Use this for changes that do not go through a model at all — a raw SQL fix-up, a migration backfill, an action taken by an external system. Convenience accessors addAuditHistoryToModel(Model) and addAuditHistoryToInstance(prototype) graft history methods on, resolved through the logger you registered with setAuditLogger(): They throw AuditLogger not initialized. Call setAuditLogger() first. if no logger has been registered. The @Audit decorator An in-memory change log for one class — no table, no persistence. Option | Effect | --- | --- | tableName | Label on entries. Default 'auditlog'. Nothing is written to a table. | trackFields | Capture only these fields in oldValues / newValues. | userField | Attribute on the model holding the acting user. | getAuditOptions(User) reads the configuration back. Entries live for the lifetime of the process — this is a debugging and testing aid, not a compliance control. Choosing a shape Track everything on every model — new AuditLogger(prorm) + setupHooks(). Simple, and the audit table grows with your write volume; plan retention. Track a few sensitive models — skip setupHooks() and call addHooksToModel() for the ones that matter. Track specific columns — @Audit({ trackFields }), or filter inside your own logAction() call. Need tamper-evidence — Compliance's AuditTrail and WORMStorage. Index the audit table on (tableName, recordId, timestamp); every read above filters on exactly those columns. Related reading Hooks — the mechanism this is built on Logging — statement-level logging Compliance — tamper-evident trails, retention, DSAR Decorators — @Audit and friends