import * as fs from 'fs';
import * as path from 'path';
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
export interface AuditLogEntry {
timestamp: string;
level: LogLevel;
user?: string;
tool: string;
action: string;
args?: Record<string, unknown>;
result?: 'success' | 'failure';
error?: string;
duration_ms?: number;
}
export interface LoggerConfig {
logDir: string;
enableConsole: boolean;
enableFile: boolean;
minLevel: LogLevel;
maskSensitiveFields: string[];
}
const LOG_LEVELS: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
const DEFAULT_SENSITIVE_FIELDS = [
'password',
'token',
'secret',
'apiKey',
'api_key',
'accessToken',
'access_token',
'refreshToken',
'refresh_token',
'privateKey',
'private_key',
'credential',
];
export class Logger {
private config: LoggerConfig;
private logStream: fs.WriteStream | null = null;
constructor(config: Partial<LoggerConfig> = {}) {
this.config = {
logDir: config.logDir ?? './logs',
enableConsole: config.enableConsole ?? true,
enableFile: config.enableFile ?? true,
minLevel: config.minLevel ?? 'info',
maskSensitiveFields: [
...DEFAULT_SENSITIVE_FIELDS,
...(config.maskSensitiveFields ?? []),
],
};
if (this.config.enableFile) {
this.initLogStream();
}
}
private initLogStream(): void {
if (!fs.existsSync(this.config.logDir)) {
fs.mkdirSync(this.config.logDir, { recursive: true });
}
const date = new Date().toISOString().split('T')[0];
const logPath = path.join(this.config.logDir, `audit-${date}.jsonl`);
this.logStream = fs.createWriteStream(logPath, { flags: 'a' });
}
private shouldLog(level: LogLevel): boolean {
return LOG_LEVELS[level] >= LOG_LEVELS[this.config.minLevel];
}
private maskSensitiveData(obj: unknown): unknown {
if (obj === null || obj === undefined) {
return obj;
}
if (typeof obj === 'string') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => this.maskSensitiveData(item));
}
if (typeof obj === 'object') {
const masked: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (this.config.maskSensitiveFields.some((field) =>
key.toLowerCase().includes(field.toLowerCase())
)) {
masked[key] = '***MASKED***';
} else {
masked[key] = this.maskSensitiveData(value);
}
}
return masked;
}
return obj;
}
private formatEntry(entry: AuditLogEntry): string {
const maskedEntry = {
...entry,
args: entry.args ? this.maskSensitiveData(entry.args) : undefined,
};
return JSON.stringify(maskedEntry);
}
private write(entry: AuditLogEntry): void {
if (!this.shouldLog(entry.level)) {
return;
}
const formatted = this.formatEntry(entry);
if (this.config.enableConsole) {
const color = this.getColor(entry.level);
const reset = '\x1b[0m';
const timestamp = entry.timestamp.substring(11, 19);
const prefix = `${color}[${timestamp}] [${entry.level.toUpperCase()}]${reset}`;
// eslint-disable-next-line no-console
console.log(`${prefix} ${entry.tool}:${entry.action} ${entry.result ?? ''}`);
if (entry.error) {
console.error(` Error: ${entry.error}`);
}
}
if (this.config.enableFile && this.logStream) {
this.logStream.write(formatted + '\n');
}
}
private getColor(level: LogLevel): string {
switch (level) {
case 'debug':
return '\x1b[90m'; // gray
case 'info':
return '\x1b[36m'; // cyan
case 'warn':
return '\x1b[33m'; // yellow
case 'error':
return '\x1b[31m'; // red
default:
return '\x1b[0m';
}
}
audit(
tool: string,
action: string,
options: {
args?: Record<string, unknown>;
user?: string;
result?: 'success' | 'failure';
error?: string;
duration_ms?: number;
} = {}
): void {
this.write({
timestamp: new Date().toISOString(),
level: options.result === 'failure' ? 'error' : 'info',
tool,
action,
...options,
});
}
debug(tool: string, message: string, data?: Record<string, unknown>): void {
this.write({
timestamp: new Date().toISOString(),
level: 'debug',
tool,
action: message,
args: data,
});
}
info(tool: string, message: string, data?: Record<string, unknown>): void {
this.write({
timestamp: new Date().toISOString(),
level: 'info',
tool,
action: message,
args: data,
});
}
warn(tool: string, message: string, data?: Record<string, unknown>): void {
this.write({
timestamp: new Date().toISOString(),
level: 'warn',
tool,
action: message,
args: data,
});
}
error(tool: string, message: string, error?: Error | string): void {
this.write({
timestamp: new Date().toISOString(),
level: 'error',
tool,
action: message,
error: error instanceof Error ? error.message : error,
});
}
close(): void {
if (this.logStream) {
this.logStream.end();
this.logStream = null;
}
}
}
// Singleton instance
let loggerInstance: Logger | null = null;
export function getLogger(config?: Partial<LoggerConfig>): Logger {
if (!loggerInstance) {
loggerInstance = new Logger(config);
}
return loggerInstance;
}
export function resetLogger(): void {
if (loggerInstance) {
loggerInstance.close();
loggerInstance = null;
}
}