@nestbolt/audit-log
Events
Listen for audit log events to trigger notifications, analytics, or other downstream actions.
When @nestjs/event-emitter is installed and registered, the module emits an event every time an audit log entry is created -- whether through the automatic subscriber or via manual AuditLogService.log() calls.
Prerequisites
Install and register the event emitter module:
npm install @nestjs/event-emitterimport { Module } from "@nestjs/common";
import { EventEmitterModule } from "@nestjs/event-emitter";
import { AuditLogModule } from "@nestbolt/audit-log";
@Module({
imports: [
EventEmitterModule.forRoot(),
AuditLogModule.forRoot(),
],
})
export class AppModule {}The AuditLogModule automatically detects the event emitter when it is available. No additional configuration is required.
If @nestjs/event-emitter is not installed, the module operates normally without emitting events. There are no errors or warnings.
The audit.logged Event
| Property | Value |
|---|---|
| Event name | audit.logged |
| Constant | AUDIT_LOG_EVENTS.LOGGED |
| Payload type | AuditLoggedEvent |
| Emitted when | After any audit log entry is persisted to the database |
Event Payload
interface AuditLoggedEvent {
auditLog: AuditLogEntity;
}The payload contains the full saved AuditLogEntity, including all columns:
{
auditLog: {
id: "550e8400-e29b-41d4-a716-446655440000",
action: "updated",
entityType: "User",
entityId: "abc-123",
actorType: "Admin",
actorId: "admin-456",
oldValues: { role: "member" },
newValues: { role: "admin" },
metadata: { app: "my-app" },
ipAddress: null,
userAgent: null,
createdAt: "2025-06-15T10:30:00.000Z"
}
}Listening for Events
Basic Listener
import { Injectable } from "@nestjs/common";
import { OnEvent } from "@nestjs/event-emitter";
import { AUDIT_LOG_EVENTS, AuditLoggedEvent } from "@nestbolt/audit-log";
@Injectable()
export class AuditLogListener {
@OnEvent(AUDIT_LOG_EVENTS.LOGGED)
handleAuditLog(event: AuditLoggedEvent): void {
const { auditLog } = event;
console.log(
`[AUDIT] ${auditLog.action} ${auditLog.entityType}#${auditLog.entityId}` +
` by ${auditLog.actorType ?? "unknown"}:${auditLog.actorId ?? "unknown"}`,
);
}
}Register the listener as a provider in a module:
import { Module } from "@nestjs/common";
import { AuditLogListener } from "./audit-log.listener";
@Module({
providers: [AuditLogListener],
})
export class AuditModule {}Filtering by Action
@Injectable()
export class DeletionAlertListener {
@OnEvent(AUDIT_LOG_EVENTS.LOGGED)
handleAuditLog(event: AuditLoggedEvent): void {
if (event.auditLog.action !== "deleted") return;
// Send an alert when any entity is deleted
this.alertService.send({
title: `${event.auditLog.entityType} deleted`,
message: `${event.auditLog.entityType}#${event.auditLog.entityId} was deleted` +
` by ${event.auditLog.actorType}:${event.auditLog.actorId}`,
});
}
}Filtering by Entity Type
@Injectable()
export class UserChangeNotifier {
constructor(private readonly notificationService: NotificationService) {}
@OnEvent(AUDIT_LOG_EVENTS.LOGGED)
async handleAuditLog(event: AuditLoggedEvent): Promise<void> {
const { auditLog } = event;
if (auditLog.entityType !== "User") return;
// Notify when a user's role changes
if (auditLog.action === "updated" && auditLog.newValues?.role) {
await this.notificationService.notifyRoleChange(
auditLog.entityId,
auditLog.oldValues?.role,
auditLog.newValues.role,
auditLog.actorId,
);
}
}
}Use Cases
External Audit Trail Replication
Forward audit logs to an external system (Elasticsearch, S3, a dedicated audit service):
@Injectable()
export class ExternalAuditReplicator {
constructor(private readonly httpService: HttpService) {}
@OnEvent(AUDIT_LOG_EVENTS.LOGGED)
async replicate(event: AuditLoggedEvent): Promise<void> {
const { auditLog } = event;
try {
await this.httpService.axiosRef.post("https://audit.internal.example.com/logs", {
id: auditLog.id,
action: auditLog.action,
entityType: auditLog.entityType,
entityId: auditLog.entityId,
actorType: auditLog.actorType,
actorId: auditLog.actorId,
oldValues: auditLog.oldValues,
newValues: auditLog.newValues,
metadata: auditLog.metadata,
createdAt: auditLog.createdAt,
});
} catch (error) {
// Log the failure but do not block the main operation
console.error("Failed to replicate audit log:", error);
}
}
}Real-time Dashboard Updates
Emit audit events to connected WebSocket clients:
import { Injectable } from "@nestjs/common";
import { OnEvent } from "@nestjs/event-emitter";
import { WebSocketGateway, WebSocketServer } from "@nestjs/websockets";
import { Server } from "socket.io";
import { AUDIT_LOG_EVENTS, AuditLoggedEvent } from "@nestbolt/audit-log";
@WebSocketGateway()
@Injectable()
export class AuditWebSocketGateway {
@WebSocketServer()
server!: Server;
@OnEvent(AUDIT_LOG_EVENTS.LOGGED)
handleAuditLog(event: AuditLoggedEvent): void {
const { auditLog } = event;
this.server.to("audit-dashboard").emit("audit:new", {
id: auditLog.id,
action: auditLog.action,
entityType: auditLog.entityType,
entityId: auditLog.entityId,
actorType: auditLog.actorType,
actorId: auditLog.actorId,
createdAt: auditLog.createdAt,
});
}
}Analytics and Metrics
Track audit events for monitoring or metrics:
@Injectable()
export class AuditMetricsCollector {
constructor(private readonly metricsService: MetricsService) {}
@OnEvent(AUDIT_LOG_EVENTS.LOGGED)
handleAuditLog(event: AuditLoggedEvent): void {
const { auditLog } = event;
this.metricsService.incrementCounter("audit_log_total", {
action: auditLog.action,
entity_type: auditLog.entityType,
actor_type: auditLog.actorType ?? "unknown",
});
}
}Event Timing
The audit.logged event is emitted synchronously after the audit log entry is saved to the database. This means:
- The
auditLogin the event payload has a generatedidandcreatedAtvalue. - Listeners run within the same execution context as the operation that triggered the audit log.
- For automatic (subscriber-based) logging, the event is emitted after the entity operation's transaction has committed the audit log row.
- For manual logging via
AuditLogService.log(), the event is emitted immediately after thesave()call.
If a listener throws an error, it does not affect the audit log entry (which has already been persisted) or the original entity operation.
Using the Event Name Constant
Always use the AUDIT_LOG_EVENTS.LOGGED constant rather than the string "audit.logged" to avoid typos and benefit from TypeScript autocompletion:
import { AUDIT_LOG_EVENTS } from "@nestbolt/audit-log";
// Correct
@OnEvent(AUDIT_LOG_EVENTS.LOGGED)
// Avoid
@OnEvent("audit.logged")Behavior Without @nestjs/event-emitter
If @nestjs/event-emitter is not installed or EventEmitterModule is not registered, the module works normally. The internal emit() call is a no-op when the event emitter is not available. There is no need for conditional configuration.