Register a Custom Audit Hook #
This guide explains how to record custom user actions in the audit trail from your own plugin or module.
The following concepts are expected to be known:
Overview #
auditTrailRegistry is a singleton exported from the backend package. Its register() method declares a custom audit hook and must be called before app.start() (typically in a plugin or module constructor). At startup, AuditTrailPlugin.init() reads all declared hooks from the registry and attaches two Kuzzle event listeners for each controller/action pair: one for the success case and one for the error case. Both write a document to the tenant's audit-trail collection.
Declared action and status keys are stored in Redis (keys audit-trail:registry:actions and audit-trail:registry:statuses). This means the registry is shared across all nodes in a cluster. The registry is rebuilt from scratch on every boot: on startup the plugin flushes the two Redis sets and then re-adds every key registered in the current process, ensuring no stale keys from a previous deployment persist. The flush+seed sequence is protected by a cluster-wide mutex so concurrent node restarts cannot race.
Step 1 - Call auditTrailRegistry.register() in the constructor #
Call auditTrailRegistry.register() in the constructor of your plugin or module, before app.start() is called. This is the only supported call site.
From a Plugin (internal):
import { auditTrailRegistry } from '../auditTrail/AuditTrailRegistry';
export class MyPlugin extends Plugin {
constructor() {
super({ kuzzleVersion: '>=2.53.0 <3' });
auditTrailRegistry.register({
controller: 'my-plugin/things',
action: 'create',
i18n: { action: 'createThing' },
getObject: (request) => ({ id: request.input.args._id, type: 'myThing' }),
});
}
async init(config: JSONObject, context: PluginContext) {
this.config = config;
this.context = context;
}
}Adjust the relative path to AuditTrailRegistry based on your plugin's location within the package.
From a Module:
import { Module } from '@kuzzleio/iot-platform-backend';
export class MyModule extends Module {
register(): void {
this.auditTrailRegistry.register({
controller: 'my-plugin/things',
action: 'create',
i18n: { action: 'createThing' },
getObject: (request) => ({ id: request.input.args._id, type: 'myThing' }),
});
}
}auditTrailRegistry.register() must be called in a constructor, before app.start(). Calling it inside a plugin or module init() is incorrect: because all plugin init() methods run concurrently, AuditTrailPlugin.init() may have already locked the registry by the time your init() runs, causing the hook to be silently ignored with a warning log.
Step 2 - Hook configuration reference #
auditTrailRegistry.register({
controller: 'my-plugin/things',
action: 'create',
i18n: {
action: 'createThing',
status: 'thingCreated', // (optional) defaults to kzOk
error: 'thingCreateFailed', // (optional) defaults to kzError
},
getObject: (request) => ({
id: request.input.args._id,
type: 'myThing',
}),
});AuditHookConfig properties #
| Property | Type | Required | Description |
|---|---|---|---|
controller | string | Yes | The Kuzzle controller name, e.g. my-plugin/things |
action | string | Yes | The controller action name, e.g. create |
i18n.action | string | Yes | Key identifying this action in the frontend translation files. Must not start with kz, that prefix is reserved for built-in actions and is enforced immediately at registration time |
i18n.status | string | No | Key for the success status. Defaults to kzOk |
i18n.error | string | No | Key for the error status. Defaults to kzError |
getObject | (request: Request) => AuditObject | No | Callback that extracts the audited object ({ id, type }) from the request. Omit if no object is relevant |
The kz prefix is reserved for built-in platform actions. auditTrailRegistry.register() throws immediately if any of i18n.action, i18n.status, or i18n.error starts with kz. The error is raised at call time (in the constructor), not deferred to startup.
Step 3 - Register multiple hooks at once #
You can call register() as many times as needed. A common pattern is to loop over a map of actions. From a module, use this.auditTrailRegistry; from a plugin, use the directly imported auditTrailRegistry singleton:
const actionMap: Record<string, string> = {
create: 'createThing',
update: 'updateThing',
delete: 'deleteThing',
};
for (const [action, actionKey] of Object.entries(actionMap)) {
this.auditTrailRegistry.register({
controller: 'my-plugin/things',
action,
i18n: { action: actionKey },
getObject: (request) => ({
id: request.input.args._id,
type: 'myThing',
}),
});
}Step 4 - Add frontend translations #
The frontend translates action and status keys via vue-i18n. For each key you registered, you must add a matching entry to the locale files. Without a translation the frontend will display the raw key name as a fallback.
Locale files are located at:
apps/web/src/locales/en.jsonapps/web/src/locales/fr.json
Add your keys under auditTrail.action and, if you defined custom status keys, under auditTrail.status:
{
"auditTrail": {
"action": {
"createThing": "Create thing",
"updateThing": "Update thing",
"deleteThing": "Delete thing"
},
"status": {
"thingCreated": "Thing created",
"thingCreateFailed": "Thing creation failed"
}
}
}Do the same in fr.json:
{
"auditTrail": {
"action": {
"createThing": "Créer un objet",
"updateThing": "Mettre à jour un objet",
"deleteThing": "Supprimer un objet"
},
"status": {
"thingCreated": "Objet créé",
"thingCreateFailed": "Échec de la création de l'objet"
}
}
}If a translation key is missing, the frontend displays the bare key name (e.g. createThing) as a fallback. This is expected behaviour for integrator-defined keys that have not yet been translated.