echo-audit-log
v1.0.0
Published
Production-grade, schema-driven audit logging utility for Node.js backend applications with ORM adapter support
Maintainers
Readme
Echo Audit Log
Production-grade, schema-driven audit logging utility for Node.js backend applications
Echo Audit Log is an enterprise-ready npm package that provides centralized, configurable, low-coupling audit logging for backend applications. Built with TypeScript for Node.js v22+, it generates JavaScript modules with zero manual boilerplate while maintaining ORM-agnostic architecture through an adapter pattern.
Features
- ✅ Enterprise-Ready: Production-grade, deterministic, and opinionated
- ✅ Schema-Driven: Configuration-based with strong typing
- ✅ ORM-Agnostic: Adapter pattern for multiple ORMs (Sequelize included)
- ✅ Express-First: Built-in middleware for Express.js
- ✅ Fine-Grained Control: Model, column, and operation-level configuration
- ✅ Zero Coupling: No business logic dependencies
- ✅ TypeScript: Written in TypeScript, generates JavaScript ES6 modules
- ✅ Database Support: MySQL and PostgreSQL
- ✅ Comprehensive Tracking: INSERT, UPDATE, DELETE operations
Installation
npm install echo-audit-logPeer Dependencies
npm install sequelizeQuick Start
1. Run Migration
Create the audit logs table in your database:
import { createAuditLogsTable } from 'echo-audit-log';
import { sequelize } from './database.js';
await createAuditLogsTable(sequelize.getQueryInterface(), 'audit_logs');Or use as a Sequelize migration:
// migrations/XXXXXX-create-audit-logs.js
import { createAuditLogsTable, dropAuditLogsTable } from 'echo-audit-log';
export async function up(queryInterface) {
await createAuditLogsTable(queryInterface, 'audit_logs');
}
export async function down(queryInterface) {
await dropAuditLogsTable(queryInterface, 'audit_logs');
}2. Initialize Audit Logger
import { Sequelize } from 'sequelize';
import { AuditLogger, SequelizeAdapter } from 'echo-audit-log';
const sequelize = new Sequelize('mysql://user:pass@localhost:3306/mydb');
const config = {
global: {
enabled: true,
columns: {
exclude: ['password', 'token', 'secret'],
},
},
};
const adapter = new SequelizeAdapter(sequelize, config);
const auditLogger = new AuditLogger(adapter);
await auditLogger.initialize();
// Note: This only creates the audit_logs table if it doesn't exist
// It will never drop or modify existing tables3. Attach Hooks to Models
import models from './models/index.js';
auditLogger.attachHooks(models);4. Set Audit Context (Optional)
auditLogger.setContext({
userId: 123,
requestId: 'req-abc-123',
ipAddress: '192.168.1.1',
userAgent: 'Mozilla/5.0...',
});5. Use with Express Middleware
import express from 'express';
import { createAuditMiddleware } from 'echo-audit-log';
const app = express();
const auditMiddleware = createAuditMiddleware(auditLogger, {
getUserId: (req) => req.user?.id,
getRequestId: (req) => req.headers['x-request-id'],
captureIp: true,
captureUserAgent: true,
});
app.use(auditMiddleware);Configuration
Configuration Schema
interface AuditLogConfig {
global?: GlobalConfig;
models?: {
[modelName: string]: ModelConfig;
};
auditTableName?: string;
captureMetadata?: boolean;
}
interface GlobalConfig {
enabled?: boolean;
columns?: ColumnConfig;
operations?: OperationConfig;
valueStorageMode?: 'all' | 'changed';
}
interface ModelConfig {
enabled?: boolean;
columns?: ColumnConfig;
operations?: OperationConfig;
valueStorageMode?: 'all' | 'changed';
}
interface ColumnConfig {
include?: string[];
exclude?: string[];
}
interface OperationConfig {
insert?: boolean;
update?: boolean;
delete?: boolean;
}Configuration Precedence
Configuration follows a clear precedence hierarchy:
- Model-specific configuration (highest priority)
- Global configuration
- Default values (lowest priority)
Default Values
{
global: {
enabled: true,
columns: {
include: null, // All columns
exclude: [],
},
operations: {
insert: true,
update: true,
delete: true,
},
valueStorageMode: 'all',
},
auditTableName: 'audit_logs',
captureMetadata: true,
}Configuration Examples
Example 1: Basic Configuration
const config = {
global: {
enabled: true,
columns: {
exclude: ['password', 'passwordHash', 'apiKey'],
},
},
};Example 2: Model-Specific Overrides
const config = {
global: {
enabled: true,
columns: {
exclude: ['password', 'token'],
},
valueStorageMode: 'changed',
},
models: {
User: {
columns: {
exclude: ['password', 'passwordHash', 'resetToken'],
},
valueStorageMode: 'all',
},
Session: {
enabled: false,
},
Product: {
columns: {
include: ['name', 'price', 'quantity'],
},
},
},
};Example 3: Operation Control
const config = {
global: {
operations: {
insert: true,
update: true,
delete: true,
},
},
models: {
Order: {
operations: {
delete: false,
},
},
},
};Example 4: Value Storage Modes
const config = {
global: {
valueStorageMode: 'changed',
},
models: {
User: {
valueStorageMode: 'all',
},
},
};Value Storage Modes:
'all'(default): Store all field values in old_values and new_values'changed': Store only changed fields in old_values and new_values
Audit Log Table Schema
The package creates an audit_logs table with the following structure:
| Column | Type | Description |
|--------|------|-------------|
| id | BIGINT | Primary key (auto-increment) |
| entity_id | BIGINT | Primary key value of the audited model |
| entity_name | VARCHAR(255) | Name of the model |
| action_type | ENUM | Operation type (INSERT, UPDATE, DELETE) |
| old_values | JSON | Column values before operation (NULL for INSERT) |
| new_values | JSON | Column values after operation (NULL for DELETE) |
| request_id | VARCHAR(64) | Request ID for tracing |
| ip_address | VARCHAR(45) | Client IP address (IPv4/IPv6 support) |
| user_agent | VARCHAR(255) | User agent string |
| action_by | BIGINT | User ID who performed the action (NULL for background jobs) |
| action_timestamp | TIMESTAMP | When the operation was performed |
Indexes:
idx_audit_logs_entityon (entity_name,entity_id)idx_audit_logs_timestampon (action_timestamp)idx_audit_logs_action_byon (action_by)
API Reference
AuditLogger
Main class for managing audit logging.
Methods
constructor(adapter: BaseAdapter): Create a new audit loggerasync initialize(): Initialize the audit logger and create audit_logs table (if not exists)attachHooks(models): Attach audit hooks to modelsdetachHooks(models): Detach audit hooks from modelssetContext(context: AuditContext): Set audit context for current operationclearContext(): Clear audit contextgetContext(): Get current audit contextisInitialized(): Check if logger is initialized
SequelizeAdapter
Sequelize ORM adapter for audit logging.
Constructor
new SequelizeAdapter(sequelize: Sequelize, config: AuditLogConfig)createAuditMiddleware
Express middleware factory for automatic context management.
Options
interface AuditMiddlewareOptions {
getUserId?: (req: Request) => number | null | undefined;
getRequestId?: (req: Request) => string | undefined;
captureIp?: boolean;
captureUserAgent?: boolean;
}Advanced Usage
Custom Adapter Implementation
Create your own adapter for other ORMs:
import { BaseAdapter } from 'echo-audit-log';
class MyORMAdapter extends BaseAdapter {
async initialize() {
// Initialize audit log table
}
attachHooks(models) {
// Attach hooks to models
}
detachHooks(models) {
// Detach hooks from models
}
}Manual Context Management
auditLogger.setContext({
userId: 123,
requestId: 'background-job-456',
});
await performDatabaseOperations();
auditLogger.clearContext();Background Jobs
For background jobs without user context:
auditLogger.setContext({
requestId: 'cron-job-data-import',
});
// Your business logic here (e.g., data import, processing, etc.)
await importDataFromExternalSource();
auditLogger.clearContext();Note: The package does NOT provide functionality to delete or cleanup audit logs. Audit logs are immutable records and should be retained according to your compliance requirements. If you need to archive old logs, implement your own archival strategy outside of this package.
Best Practices
1. Security
Always exclude sensitive fields:
const config = {
global: {
columns: {
exclude: [
'password',
'passwordHash',
'apiKey',
'secret',
'token',
'refreshToken',
'resetToken',
'verificationToken',
],
},
},
};2. Performance
For high-volume tables, consider:
- Using
valueStorageMode: 'changed'to reduce storage - Excluding non-critical models
const config = {
global: {
valueStorageMode: 'changed',
},
models: {
HighVolumeTable: {
valueStorageMode: 'changed',
},
SessionTable: {
enabled: false,
},
},
};3. Disable Self-Auditing
The AuditLog model is automatically disabled from auditing to prevent infinite loops. You don't need to configure this - it's built-in protection that cannot be overridden.
4. Request Tracing
Use request IDs for distributed tracing:
const auditMiddleware = createAuditMiddleware(auditLogger, {
getRequestId: (req) => req.headers['x-request-id'] || req.id,
});Error Handling
The package handles errors gracefully:
- Audit failures are logged to console but don't interrupt application flow
- Invalid configurations throw errors during initialization
- Missing dependencies are caught at runtime
Database Support
MySQL
const sequelize = new Sequelize({
dialect: 'mysql',
host: 'localhost',
username: 'root',
password: 'password',
database: 'myapp',
});PostgreSQL
const sequelize = new Sequelize({
dialect: 'postgres',
host: 'localhost',
username: 'postgres',
password: 'password',
database: 'myapp',
});Querying Audit Logs
import { AuditLog } from 'echo-audit-log';
import { Op } from 'sequelize';
const userAudits = await AuditLog.findAll({
where: {
entityName: 'User',
entityId: 123,
},
order: [['actionTimestamp', 'DESC']],
});
const recentChanges = await AuditLog.findAll({
where: {
actionType: 'UPDATE',
actionTimestamp: {
[Op.gte]: new Date(Date.now() - 24 * 60 * 60 * 1000),
},
},
});Troubleshooting
Hooks Not Firing
Ensure hooks are attached after models are defined:
await auditLogger.initialize();
auditLogger.attachHooks(models);Missing Audit Logs
Check if the model is enabled:
const config = {
models: {
YourModel: {
enabled: true,
},
},
};Performance Issues
- Use
valueStorageMode: 'changed' - Exclude high-volume, low-value tables
- Add database indexes on frequently queried columns
Requirements
- Node.js >= 22.0.0
- Sequelize >= 6.0.0
- MySQL or PostgreSQL database
License
MIT
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
Support
For issues, questions, or feature requests, please open an issue on GitHub.
Built with ❤️ for enterprise Node.js applications
