- Add Seq centralized logging transport with async ESM import - Add appVersion and computerName to logger defaultMeta (all app logs) - Add appVersion to audit log entries for version-level traceability - Improve unhandledRejection to capture full stack traces for Error instances - Add Seq config schema and template configuration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
129 lines
3.6 KiB
TypeScript
129 lines
3.6 KiB
TypeScript
/**
|
|
* Audit Logger Service
|
|
* Writes audit logs in JSONL format with 30-day rotation using winston-daily-rotate-file
|
|
*/
|
|
|
|
import winston from 'winston'
|
|
import DailyRotateFile from 'winston-daily-rotate-file'
|
|
import path from 'path'
|
|
import { app } from 'electron'
|
|
import { getLogDir } from './shared'
|
|
|
|
/**
|
|
* Audit log entry structure
|
|
* All 8 required fields for comprehensive audit tracking
|
|
*/
|
|
export interface AuditEntry {
|
|
/** ISO 8601 timestamp of the audit event */
|
|
timestamp: string
|
|
/** The action that was performed (e.g., 'LOGIN', 'EXTRACT', 'DELETE') */
|
|
action: string
|
|
/** User ID who performed the action */
|
|
userId: string
|
|
/** Username of the user who performed the action */
|
|
username: string
|
|
/** Computer name from which the action was performed */
|
|
computerName: string
|
|
/** Application version when the action was performed */
|
|
appVersion: string
|
|
/** The resource that was affected (e.g., table name, file path) */
|
|
resource: string
|
|
/** Status of the action: 'success' | 'failure' | 'partial' */
|
|
status: 'success' | 'failure' | 'partial'
|
|
/** Additional metadata about the audit event */
|
|
metadata: Record<string, unknown>
|
|
}
|
|
|
|
/**
|
|
* JSONL formatter - outputs one JSON object per line
|
|
* This is the key difference from the standard JSON formatter
|
|
*/
|
|
const jsonlFormat = winston.format.printf(({ message }) => {
|
|
// Message should already be a JSON string
|
|
return typeof message === 'string' ? message : JSON.stringify(message)
|
|
})
|
|
|
|
/**
|
|
* Create the audit logger instance with daily rotation
|
|
* Initially silent (no transports). Call applyAuditConfig() after config is loaded.
|
|
*/
|
|
const auditLogger = winston.createLogger({
|
|
level: 'info',
|
|
silent: true,
|
|
transports: []
|
|
})
|
|
|
|
/**
|
|
* Apply audit log retention configuration
|
|
* Creates the DailyRotateFile transport with the configured retention period
|
|
*
|
|
* @param retentionDays - Number of days to retain audit logs
|
|
*/
|
|
export function applyAuditConfig(retentionDays: number): void {
|
|
// Enable logging now that config is loaded
|
|
auditLogger.silent = false
|
|
|
|
// Remove existing DailyRotateFile transports
|
|
const existingTransports = auditLogger.transports.filter((t) => t instanceof DailyRotateFile)
|
|
for (const transport of existingTransports) {
|
|
auditLogger.remove(transport)
|
|
}
|
|
|
|
// Add audit transport with configured retention
|
|
auditLogger.add(
|
|
new DailyRotateFile({
|
|
filename: path.join(getLogDir(), 'audit-%DATE%.jsonl'),
|
|
datePattern: 'YYYY-MM-DD',
|
|
zippedArchive: true,
|
|
maxSize: '20m',
|
|
maxFiles: `${retentionDays}d`,
|
|
level: 'info',
|
|
format: jsonlFormat
|
|
})
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Log an audit event
|
|
*
|
|
* @param action - The action that was performed
|
|
* @param userId - User ID who performed the action
|
|
* @param details - Additional details including username, computerName, resource, status, and optional metadata
|
|
*/
|
|
export function logAudit(
|
|
action: string,
|
|
userId: string,
|
|
details: {
|
|
username: string
|
|
computerName: string
|
|
resource: string
|
|
status: 'success' | 'failure' | 'partial'
|
|
metadata?: Record<string, unknown>
|
|
}
|
|
): void {
|
|
const entry: AuditEntry = {
|
|
timestamp: new Date().toISOString(),
|
|
action,
|
|
userId,
|
|
username: details.username,
|
|
computerName: details.computerName,
|
|
appVersion: app.getVersion(),
|
|
resource: details.resource,
|
|
status: details.status,
|
|
metadata: details.metadata || {}
|
|
}
|
|
|
|
// Write as JSONL - one JSON object per line
|
|
// Using info level with the entry stringified as the message
|
|
auditLogger.info(JSON.stringify(entry))
|
|
}
|
|
|
|
/**
|
|
* Flush and close the audit logger (call on app shutdown)
|
|
*/
|
|
export function closeAuditLogger(): void {
|
|
auditLogger.close()
|
|
}
|
|
|
|
export default auditLogger
|