@rollercoaster-dev/rd-logger
Version:
A neurodivergent-friendly logger for Rollercoaster.dev projects
227 lines (226 loc) • 8.68 kB
JavaScript
import { DEFAULT_LOGGER_CONFIG, LOG_LEVEL_PRIORITY, } from './logger.config';
import { ConsoleTransport, FileTransport } from './transports';
import { TextFormatter } from './formatters';
import { formatError } from './utils';
/**
* Enhanced neuro-friendly logger class
*/
export class Logger {
constructor(options) {
this.transports = [];
this.config = Object.assign(Object.assign({}, DEFAULT_LOGGER_CONFIG), options);
this.formatter = this.config.formatter || new TextFormatter();
// Initialize transports
this.initializeTransports();
}
/**
* Initialize transports based on configuration
*/
initializeTransports() {
// Clear existing transports
this.transports = [];
// Use custom transports if provided
if (this.config.transports && this.config.transports.length > 0) {
this.transports = [...this.config.transports];
}
else {
// Otherwise, set up default transports based on config
// Always add console transport by default
this.transports.push(new ConsoleTransport({
prettyPrint: this.config.prettyPrint,
colorize: this.config.colorize,
use24HourFormat: this.config.use24HourFormat,
levelColors: this.config.levelColors,
levelIcons: this.config.levelIcons,
}));
// Add file transport if enabled
if (this.config.logToFile) {
const fileTransport = new FileTransport({
filePath: this.config.logFilePath,
});
// Add the transport; initialization will happen lazily on first log
this.transports.push(fileTransport);
}
}
}
/**
* Main logging function
* @param level Log level
* @param message Log message
* @param context Additional context (optional)
*/
log(level, message, context = {}) {
// Check if this log level should be shown based on configuration
// In LOG_LEVEL_PRIORITY, higher values mean less verbose (debug=0, fatal=4)
// So we only log if the message level value is >= the configured level value
// For example, if config.level is 'info' (1), we log 'info' (1), 'warn' (2), 'error' (3), 'fatal' (4), but not 'debug' (0)
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
return;
}
// Process context - handle errors specially
let processedContext = Object.assign({}, context);
if (context.error instanceof Error) {
processedContext = Object.assign(Object.assign({}, processedContext), formatError(context.error, this.config.includeStackTrace));
delete processedContext.error;
}
// Get timestamp
const timestamp = new Date().toISOString();
// Send to all transports
for (const transport of this.transports) {
transport.log(level, message, timestamp, processedContext);
}
}
// Convenience wrappers
debug(msg, ctx) {
this.log('debug', msg, ctx);
}
info(msg, ctx) {
this.log('info', msg, ctx);
}
warn(msg, ctx) {
this.log('warn', msg, ctx);
}
error(msg, ctx) {
this.log('error', msg, ctx);
}
fatal(msg, ctx) {
this.log('fatal', msg, ctx);
}
/**
* Logs an error object directly
* @param msg Prefix message
* @param error Error object
* @param additionalContext Additional context (optional)
*/
logError(msg, error, additionalContext = {}) {
this.log('error', msg, Object.assign(Object.assign({}, additionalContext), { error }));
}
/**
* Explicitly log sensitive data with approval information
* This method should only be used in exceptional circumstances where logging sensitive data is necessary
* @param level Log level
* @param message Log message
* @param data Data containing sensitive information
* @param approval Approval information for logging sensitive data
*/
logWithSensitiveData(level, message, data, approval) {
// Validate approval
if (!approval.reason || !approval.approvedBy) {
this.warn('Attempted to log sensitive data without proper approval', {
message: 'Missing required approval information. Sensitive data will not be logged.',
});
return;
}
// Check if approval has expired
if (approval.expiresAt && new Date() > approval.expiresAt) {
this.warn('Attempted to log sensitive data with expired approval', {
message: 'Approval has expired. Sensitive data will not be logged.',
expiredAt: approval.expiresAt,
});
return;
}
// Add approval information to the context
const contextWithApproval = Object.assign(Object.assign({}, data), { __sensitive_data_approval__: {
reason: approval.reason,
approvedBy: approval.approvedBy,
approvedAt: new Date().toISOString(),
expiresAt: approval.expiresAt
? approval.expiresAt.toISOString()
: undefined,
} });
// Log with a warning prefix to make it stand out
const warningPrefix = '⚠️ SENSITIVE DATA ⚠️ ';
this.log(level, `${warningPrefix}${message}`, contextWithApproval);
}
/**
* Convenience method for logging sensitive data with info level
*/
infoWithSensitiveData(message, data, approval) {
this.logWithSensitiveData('info', message, data, approval);
}
/**
* Convenience method for logging sensitive data with error level
*/
errorWithSensitiveData(message, data, approval) {
this.logWithSensitiveData('error', message, data, approval);
}
/**
* Update the logger's configuration dynamically
* @param options Partial configuration options to update
*/
configure(options) {
// We don't need oldConfig for now, but it might be useful for future comparisons
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const oldConfig = Object.assign({}, this.config);
this.config = Object.assign(Object.assign({}, this.config), options);
// Update formatter if provided
if (options.formatter) {
this.formatter = options.formatter;
}
// Reinitialize transports if relevant config changed
const transportConfigChanged = options.transports !== undefined ||
options.logToFile !== undefined ||
options.logFilePath !== undefined ||
options.prettyPrint !== undefined ||
options.colorize !== undefined ||
options.use24HourFormat !== undefined ||
options.levelColors !== undefined ||
options.levelIcons !== undefined;
if (transportConfigChanged) {
this.initializeTransports();
}
}
/**
* Set the log level dynamically
* @param level New log level to set
*/
setLevel(level) {
this.configure({ level });
}
/**
* Update configuration options dynamically
* @param options Partial configuration options to update
* @alias configure - Provided for API consistency
*/
updateConfig(options) {
this.configure(options);
}
/**
* Add a transport to the logger
* @param transport Transport to add
*/
addTransport(transport) {
this.transports.push(transport);
}
/**
* Remove a transport from the logger by name
* @param name Name of the transport to remove
* @returns Whether the transport was found and removed
*/
removeTransport(name) {
const initialLength = this.transports.length;
this.transports = this.transports.filter((t) => t.name !== name);
return this.transports.length < initialLength;
}
/**
* Set the formatter for the logger
* @param formatter Formatter to use
*/
setFormatter(formatter) {
this.formatter = formatter;
}
/**
* Clean up resources used by the logger
* Should be called when the logger is no longer needed
*/
cleanup() {
// Clean up all transports
for (const transport of this.transports) {
if (transport.cleanup) {
transport.cleanup();
}
}
// Clear transports array
this.transports = [];
}
}