DocumentationReference

HazelJS Audit Package

npm downloads

@hazeljs/​audit provides audit logging and event trails for HazelJS — record who did what, when, and with what outcome. Events go to console, file (JSONL with rotation), or Kafka (JSON or Avro).

Quick Reference

  • Purpose: @hazeljs/​audit provides audit logging for compliance, security, and debugging — capturing HTTP requests, business events, actors, and outcomes with pluggable sinks (console, file, Kafka).
  • When to use: Use @hazeljs/​audit when a HazelJS application needs compliance audit trails, security logging, or event tracking for debugging.
  • Key concepts: AuditInterceptor (opt-in HTTP audit), @Audit decorator (action/resource/result metadata), audit transports (console, file with JSONL rotation, Kafka with JSON/Avro), actor tracking.
  • Dependencies: @hazeljs/​core, optionally @hazeljs/​kafka for Kafka transport.
  • Common patterns: Register AuditModule.forRoot() with transport config → attach AuditInterceptor on controllers/routes → use @Audit for semantic action names → call AuditService.log() for business events.
  • Common mistakes: Expecting audit without attaching AuditInterceptor; not configuring file rotation (disk fills up); logging extra user fields instead of actorFromContext().

Purpose

Applications often need a reliable audit trail for compliance, security, and debugging. Building this from scratch involves capturing HTTP requests, business events, actors, and outcomes, then shipping them to logs or external systems. The @hazeljs/​audit package simplifies this by providing:

  • HTTP audit — Attach AuditInterceptor to log handled requests (method, path, result)
  • Custom events — Inject AuditService and call log() with action, resource, actor
  • Pluggable transports — Console (default), file (JSONL with rotation), Kafka (JSON or Avro)
  • @Audit decorator — Mark handlers with action/resource for metadata and tooling
  • Redaction — Sensitive keys (password, token, etc.) redacted by default
  • Actor from contextactorFromContext() maps request user to audit actor

Architecture

The package uses a transport-based architecture: events flow through AuditService to all configured transports.

graph TD
  A["HTTP Request / Business Logic"] --> B["AuditInterceptor<br/>or AuditService.log()"]
  B --> C["AuditService<br/>(Sanitize, Redact)"]
  C --> D["Transports"]
  D --> E["ConsoleAuditTransport"]
  D --> F["FileAuditTransport"]
  D --> G["KafkaAuditTransport"]
  D --> H["Custom Transport"]
  
  style A fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style B fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff
  style C fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style D fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style E fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style F fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style G fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style H fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff

Key Components

  1. AuditModule — Registers the module via forRoot(options)
  2. AuditService — Injected service; call log(event) and actorFromContext(context)
  3. AuditInterceptor — Opt-in HTTP request audit when attached to controllers or routes
  4. Transports — Console, File, Kafka; implement AuditTransport for custom backends
  5. @Audit decorator — Metadata for action/resource on handler methods

Advantages

1. Compliance-ready

Structured events with action, actor, resource, result, and timestamp suit compliance and security reviews.

2. Multiple outputs

Send the same events to stdout, file, and Kafka (or your own transport) without duplicating logic.

3. Redaction by default

Sensitive keys in metadata are redacted so you don’t log passwords or tokens by mistake.

4. Kafka and Avro

Optional Kafka transport with custom serialization (e.g. Avro via serialize) for event pipelines.

5. Simple API

One interceptor for HTTP; one audit.log() call for business events. Actor is derived from request context when available.

6. Type safety

Full TypeScript support for AuditEvent, transports, and decorator options.

Installation

npm install @hazeljs/​audit @hazeljs/​core

Quick Start

1. Register the module and attach the interceptor

import { HazelModule, Controller, Get, UseInterceptors } from '@hazeljs/​core';
import { AuditModule, AuditInterceptor, Audit } from '@hazeljs/​audit';

@HazelModule({
  imports: [AuditModule.forRoot()],
})
export class AppModule {}

@Controller('/​orders')
@UseInterceptors(AuditInterceptor)
export class OrderController {
  @Get()
  @Audit({ action: 'order.list', resource: 'Order' })
  list() {
    return this.orderService.findAll();
  }
}

Registering AuditModule provides AuditService and AuditInterceptor. HTTP requests are audited only where you attach the interceptor. Use AuditService.log() for custom business events.

2. Module options

AuditModule.forRoot({
  transports: [new ConsoleAuditTransport()], /​/​ default
  includeRequestContext: true,
  redactKeys: ['password', 'token', 'secret', 'authorization'],
  redactNested: true,
  actorFields: ['id', 'username', 'role'],
  interceptor: {
    excludePaths: ['/​health', '/​metrics'],
  },
});

3. Custom events with AuditService

import { Service } from '@hazeljs/​core';
import { AuditService } from '@hazeljs/​audit';
import type { RequestContext } from '@hazeljs/​core';

@Service()
export class OrderService {
  constructor(private readonly audit: AuditService) {}

  async create(data: CreateOrderDto, context: RequestContext) {
    const order = await this.repo.create(data);
    this.audit.log({
      action: 'order.create',
      actor: this.audit.actorFromContext(context),
      resource: 'Order',
      resourceId: order.id,
      result: 'success',
      metadata: { amount: order.total },
    });
    return order;
  }
}

Audit event shape

  • action — e.g. user.login, order.create
  • actor{ id, username?, role? } from request context when available
  • resource / resourceId — what was affected
  • resultsuccess | failure | denied (denied when a guard throws UnauthorizedError)
  • timestamp — ISO string (set automatically if omitted)
  • requestId — from correlation headers or router-generated id
  • method / path — from HTTP context when includeRequestContext is true (default)
  • metadata — extra structured data (sensitive keys redacted by default, nested when redactNested is true)

Transports

Console (default)

Writes one JSON line per event to stdout.

import { ConsoleAuditTransport } from '@hazeljs/​audit';

transports: [new ConsoleAuditTransport()],

File (JSONL)

Appends to a file. Creates the file and parent dir on first event. Supports rotation by size or by day.

import { FileAuditTransport } from '@hazeljs/​audit';

transports: [
  new FileAuditTransport({
    filePath: 'logs/​audit.jsonl',
    ensureDir: true,
    maxSizeBytes: 10 * 1024 * 1024, /​/​ 10MB
    rollDaily: true, /​/​ audit.2025-03-01.jsonl
  }),
],

Kafka

Sends each event to a Kafka topic. Use with @hazeljs/kafka: pass KafkaProducerService as the sender. Optional key for partitioning; optional serialize for custom format (default JSON). For Avro, pass a serialize function that returns a Buffer.

import { KafkaAuditTransport } from '@hazeljs/​audit';
import { KafkaProducerService } from '@hazeljs/​kafka';

const transport = new KafkaAuditTransport({
  sender: kafkaProducerService, /​/​ from your app's container
  topic: 'audit',
  key: (e) => e.actor?.id?.toString(), /​/​ optional partition key
  /​/​ serialize: (e) => avroType.toBuffer(e), /​/​ optional Avro
});

You can add the Kafka transport at runtime after the app is created (e.g. after resolving KafkaProducerService) using auditService.addTransport(transport).

Custom transport

Implement the AuditTransport interface (log(event: AuditEvent)) to send events to your database, SIEM, or logging service.

@Audit decorator

When AuditInterceptor is attached, @Audit sets the audit action/resource and can include a redacted result snapshot:

import { Controller, Post, Body, UseInterceptors } from '@hazeljs/​core';
import { Audit, AuditInterceptor } from '@hazeljs/​audit';

@Controller('/​orders')
@UseInterceptors(AuditInterceptor)
export class OrderController {
  @Post()
  @Audit({ action: 'order.create', resource: 'Order', includeResult: true })
  async create(@Body() dto: CreateOrderDto) {
    return this.orderService.create(dto);
  }
}

Use getAuditMetadata(target, propertyKey) and hasAuditMetadata(target, propertyKey) to read decorator metadata in other code.

License

Apache-2.0

Recipes

Recipe: Audit Business Events with AuditService

/​/​ File: src/​orders/​order.service.ts
import { Service } from '@hazeljs/​core';
import { AuditService } from '@hazeljs/​audit';
import type { RequestContext } from '@hazeljs/​core';

@Service()
export class OrderService {
  constructor(private readonly audit: AuditService) {}

  async createOrder(userId: string, items: unknown[], context: RequestContext) {
    const order = { id: `ORD-${Date.now()}`, items, status: 'created' };

    await this.audit.log({
      action: 'order.created',
      resource: 'order',
      resourceId: order.id,
      actor: this.audit.actorFromContext(context) ?? { id: userId },
      requestId: context.requestId,
      metadata: { itemCount: items.length },
    });

    return order;
  }
}

Recipe: HTTP Audit Logging with AuditInterceptor

/​/​ File: src/​app.module.ts
import { HazelModule, Controller, Get, UseInterceptors } from '@hazeljs/​core';
import {
  AuditModule,
  AuditInterceptor,
  FileAuditTransport,
  Audit,
} from '@hazeljs/​audit';

@HazelModule({
  imports: [
    AuditModule.forRoot({
      transports: [
        new FileAuditTransport({
          filePath: '.​/logs/​audit.jsonl',
          maxSizeBytes: 10 * 1024 * 1024,
          rollDaily: true,
        }),
      ],
      interceptor: { excludePaths: ['/​health'] },
    }),
  ],
})
export class AppModule {}

@Controller('/​orders')
@UseInterceptors(AuditInterceptor)
export class OrderController {
  @Get()
  @Audit({ action: 'order.list', resource: 'Order' })
  list() {
    return [];
  }
}