logger.ts•4.9 kB
import pino from 'pino';
import { LogEntry } from '../schemas/index';
/**
* Enhanced Logger with Security Redaction
*
* Provides structured logging with automatic redaction of sensitive data
* and support for file rotation and multiple output formats.
*/
export class Logger {
private logger: pino.Logger;
private readonly redactPatterns = [
'password',
'secret',
'token',
'api_key',
'jwt',
'authorization',
'cookie',
'session',
'private_key',
];
constructor(_name: string) {
this.logger = pino({
name: _name,
level: process.env['LOG_LEVEL'] || 'info',
timestamp: pino.stdTimeFunctions.isoTime,
formatters: {
level: label => {
return { level: label };
},
bindings: bindings => {
return { ['pid']: bindings['pid'], ['hostname']: bindings['hostname'] };
},
},
redact: {
paths: this.redactPatterns,
censor: _value => '[REDACTED]',
},
});
}
debug(message: string, data?: any): void {
if (data) {
this.logger.debug(message, this.sanitizeData(data));
} else {
this.logger.debug(message);
}
}
info(message: string, data?: any): void {
if (data) {
this.logger.info(message, this.sanitizeData(data));
} else {
this.logger.info(message);
}
}
warn(message: string, data?: any): void {
if (data) {
this.logger.warn(message, this.sanitizeData(data));
} else {
this.logger.warn(message);
}
}
error(message: string, error?: Error | any): void {
if (error instanceof Error) {
this.logger.error(message, {
error: error.message,
stack: error.stack,
...this.sanitizeData({ errorName: error.name }),
});
} else if (error) {
this.logger.error(message, this.sanitizeData(error));
} else {
this.logger.error(message);
}
}
fatal(message: string, error?: Error | any): void {
if (error instanceof Error) {
this.logger.fatal(message, {
error: error.message,
stack: error.stack,
...this.sanitizeData({ errorName: error.name }),
});
} else if (error) {
this.logger.fatal(message, this.sanitizeData(error));
} else {
this.logger.fatal(message);
}
}
/**
* Sanitize data for logging by removing sensitive information
*/
private sanitizeData(data: any): any {
if (typeof data !== 'object' || data === null) {
return data;
}
const sanitized: any = Array.isArray(data) ? [] : {};
for (const [key, value] of Object.entries(data)) {
if (
typeof key === 'string' &&
this.redactPatterns.some(pattern => key.toLowerCase().includes(pattern))
) {
sanitized[key] = '[REDACTED]';
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = this.sanitizeData(value);
} else {
// Check for potential sensitive data in string values
if (typeof value === 'string' && this.isPotentialSensitiveData(value)) {
sanitized[key] = '[REDACTED]';
} else {
sanitized[key] = value;
}
}
}
return sanitized;
}
/**
* Detect potential sensitive data in string values
*/
private isPotentialSensitiveData(value: string): boolean {
// Check for API keys (hex strings)
if (/^[a-f0-9]{32,}$/i.test(value)) {
return true;
}
// Check for JWT-like patterns
if (/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/.test(value)) {
return true;
}
// Check for email addresses (might contain user info)
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return true;
}
// Check for long numeric strings (might be IDs with user info)
if (/^\d{10,}$/.test(value)) {
return true;
}
return false;
}
/**
* Create a child logger with additional context
*/
child(context: Record<string, any>): Logger {
const childLogger = new Logger('');
childLogger.logger = this.logger.child(this.sanitizeData(context));
return childLogger;
}
/**
* Log a structured log entry
*/
logEntry(entry: LogEntry): void {
const logData = {
...this.sanitizeData(entry.data),
sessionId: entry.sessionId ? this.sanitizeData(entry.sessionId) : undefined,
};
switch (entry.level) {
case 'debug':
this.logger.debug(entry.message, logData);
break;
case 'info':
this.logger.info(entry.message, logData);
break;
case 'warn':
this.logger.warn(entry.message, logData);
break;
case 'error':
this.logger.error(entry.message, logData);
break;
case 'fatal':
this.logger.fatal(entry.message, logData);
break;
default:
this.logger.info(entry.message, logData);
}
}
}