NestboltNestbolt

@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-emitter
import { 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

PropertyValue
Event nameaudit.logged
ConstantAUDIT_LOG_EVENTS.LOGGED
Payload typeAuditLoggedEvent
Emitted whenAfter 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 auditLog in the event payload has a generated id and createdAt value.
  • 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 the save() 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.