alepm
Version:
Advanced and secure Node.js package manager with binary storage, intelligent caching, and comprehensive security features
466 lines (385 loc) • 11.4 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const os = require('os');
const chalk = require('chalk');
class Logger {
constructor(options = {}) {
this.levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
verbose: 4,
debug: 5,
silly: 6
};
this.colors = {
error: 'red',
warn: 'yellow',
info: 'cyan',
http: 'green',
verbose: 'blue',
debug: 'magenta',
silly: 'gray'
};
this.config = {
level: options.level || 'info',
silent: options.silent || false,
timestamp: options.timestamp !== false,
colorize: options.colorize !== false,
json: options.json || false,
logFile: options.logFile || path.join(os.homedir(), '.alepm', 'logs', 'alepm.log'),
maxSize: options.maxSize || '10MB',
maxFiles: options.maxFiles || 5,
...options
};
this.init();
}
async init() {
await fs.ensureDir(path.dirname(this.config.logFile));
// Rotate logs if needed
await this.rotateLogsIfNeeded();
}
log(level, message, meta = {}) {
if (this.config.silent) {
return;
}
const levelNum = this.levels[level];
const configLevelNum = this.levels[this.config.level];
if (levelNum > configLevelNum) {
return;
}
const logEntry = this.formatLogEntry(level, message, meta);
// Output to console
this.outputToConsole(level, logEntry);
// Write to file
this.writeToFile(logEntry);
}
error(message, meta = {}) {
this.log('error', message, meta);
}
warn(message, meta = {}) {
this.log('warn', message, meta);
}
info(message, meta = {}) {
this.log('info', message, meta);
}
http(message, meta = {}) {
this.log('http', message, meta);
}
verbose(message, meta = {}) {
this.log('verbose', message, meta);
}
debug(message, meta = {}) {
this.log('debug', message, meta);
}
silly(message, meta = {}) {
this.log('silly', message, meta);
}
formatLogEntry(level, message, meta) {
const timestamp = this.config.timestamp ? new Date().toISOString() : null;
const entry = {
timestamp,
level,
message,
...meta
};
if (this.config.json) {
return JSON.stringify(entry);
} else {
let formatted = '';
if (timestamp) {
formatted += `[${timestamp}] `;
}
formatted += `${level.toUpperCase()}: ${message}`;
if (Object.keys(meta).length > 0) {
formatted += ` ${JSON.stringify(meta)}`;
}
return formatted;
}
}
outputToConsole(level, logEntry) {
const colorize = this.config.colorize && process.stdout.isTTY;
if (colorize) {
const color = this.colors[level] || 'white';
console.log(chalk[color](logEntry));
} else {
console.log(logEntry);
}
}
async writeToFile(logEntry) {
try {
await fs.appendFile(this.config.logFile, logEntry + '\n');
} catch (error) {
// Fail silently to avoid infinite loops
}
}
async rotateLogsIfNeeded() {
try {
const stats = await fs.stat(this.config.logFile);
const maxSizeBytes = this.parseSize(this.config.maxSize);
if (stats.size > maxSizeBytes) {
await this.rotateLogs();
}
} catch (error) {
// File doesn't exist yet, no need to rotate
}
}
async rotateLogs() {
const logDir = path.dirname(this.config.logFile);
const logBasename = path.basename(this.config.logFile, path.extname(this.config.logFile));
const logExt = path.extname(this.config.logFile);
// Rotate existing files
for (let i = this.config.maxFiles - 1; i > 0; i--) {
const oldFile = path.join(logDir, `${logBasename}.${i}${logExt}`);
const newFile = path.join(logDir, `${logBasename}.${i + 1}${logExt}`);
if (await fs.pathExists(oldFile)) {
if (i === this.config.maxFiles - 1) {
// Delete the oldest file
await fs.remove(oldFile);
} else {
await fs.move(oldFile, newFile);
}
}
}
// Move current log to .1
const currentLog = this.config.logFile;
const rotatedLog = path.join(logDir, `${logBasename}.1${logExt}`);
if (await fs.pathExists(currentLog)) {
await fs.move(currentLog, rotatedLog);
}
}
parseSize(size) {
const match = size.match(/^(\d+(?:\.\d+)?)([KMGT]?)B$/i);
if (!match) return 0;
const [, value, unit] = match;
const multipliers = { '': 1, K: 1024, M: 1024**2, G: 1024**3, T: 1024**4 };
return parseFloat(value) * (multipliers[unit.toUpperCase()] || 1);
}
// Performance logging
time(label) {
if (!this.timers) {
this.timers = new Map();
}
this.timers.set(label, process.hrtime.bigint());
}
timeEnd(label) {
if (!this.timers || !this.timers.has(label)) {
this.warn(`Timer "${label}" does not exist`);
return;
}
const start = this.timers.get(label);
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1000000; // Convert to milliseconds
this.timers.delete(label);
this.info(`${label}: ${duration.toFixed(2)}ms`);
return duration;
}
// Request logging
logRequest(method, url, options = {}) {
this.http(`${method} ${url}`, {
method,
url,
userAgent: options.userAgent,
timeout: options.timeout,
headers: this.sanitizeHeaders(options.headers)
});
}
logResponse(method, url, statusCode, duration, options = {}) {
const level = statusCode >= 400 ? 'error' : statusCode >= 300 ? 'warn' : 'http';
this.log(level, `${method} ${url} ${statusCode} ${duration}ms`, {
method,
url,
statusCode,
duration,
size: options.size
});
}
sanitizeHeaders(headers = {}) {
const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token'];
const sanitized = {};
for (const [key, value] of Object.entries(headers)) {
if (sensitiveHeaders.includes(key.toLowerCase())) {
sanitized[key] = '[REDACTED]';
} else {
sanitized[key] = value;
}
}
return sanitized;
}
// Package operation logging
logPackageOperation(operation, packageName, version, options = {}) {
this.info(`${operation} ${packageName}@${version}`, {
operation,
package: packageName,
version,
...options
});
}
logPackageError(operation, packageName, version, error, options = {}) {
this.error(`Failed to ${operation} ${packageName}@${version}: ${error.message}`, {
operation,
package: packageName,
version,
error: error.message,
stack: error.stack,
...options
});
}
logCacheOperation(operation, key, options = {}) {
this.debug(`Cache ${operation}: ${key}`, {
operation,
key,
...options
});
}
logSecurityEvent(event, details = {}) {
this.warn(`Security event: ${event}`, {
event,
timestamp: Date.now(),
...details
});
}
// Structured logging for analytics
logAnalytics(event, data = {}) {
this.info(`Analytics: ${event}`, {
event,
timestamp: Date.now(),
session: this.getSessionId(),
platform: process.platform,
arch: process.arch,
node: process.version,
...data
});
}
getSessionId() {
if (!this.sessionId) {
this.sessionId = require('crypto').randomUUID();
}
return this.sessionId;
}
// Progress logging
createProgressLogger(total, label = 'Progress') {
let current = 0;
let lastLogTime = 0;
const minLogInterval = 1000; // Log at most once per second
return {
tick: (amount = 1) => {
current += amount;
const now = Date.now();
if (now - lastLogTime > minLogInterval || current >= total) {
const percentage = ((current / total) * 100).toFixed(1);
this.info(`${label}: ${current}/${total} (${percentage}%)`);
lastLogTime = now;
}
},
complete: () => {
this.info(`${label}: Complete (${total}/${total})`);
}
};
}
// Error aggregation
reportErrors() {
if (!this.errorStats) {
return { totalErrors: 0, errorTypes: {} };
}
return {
totalErrors: this.errorStats.total,
errorTypes: { ...this.errorStats.types },
lastError: this.errorStats.lastError
};
}
trackError(error) {
if (!this.errorStats) {
this.errorStats = {
total: 0,
types: {},
lastError: null
};
}
this.errorStats.total++;
this.errorStats.types[error.constructor.name] =
(this.errorStats.types[error.constructor.name] || 0) + 1;
this.errorStats.lastError = {
message: error.message,
timestamp: Date.now()
};
}
// Log file management
async clearLogs() {
try {
const logDir = path.dirname(this.config.logFile);
const logBasename = path.basename(this.config.logFile, path.extname(this.config.logFile));
const logExt = path.extname(this.config.logFile);
// Remove all log files
await fs.remove(this.config.logFile);
for (let i = 1; i <= this.config.maxFiles; i++) {
const logFile = path.join(logDir, `${logBasename}.${i}${logExt}`);
if (await fs.pathExists(logFile)) {
await fs.remove(logFile);
}
}
this.info('Log files cleared');
} catch (error) {
this.error('Failed to clear logs', { error: error.message });
}
}
async getLogStats() {
try {
const stats = {
files: [],
totalSize: 0
};
const logDir = path.dirname(this.config.logFile);
const logBasename = path.basename(this.config.logFile, path.extname(this.config.logFile));
const logExt = path.extname(this.config.logFile);
// Check main log file
if (await fs.pathExists(this.config.logFile)) {
const stat = await fs.stat(this.config.logFile);
stats.files.push({
file: this.config.logFile,
size: stat.size,
modified: stat.mtime
});
stats.totalSize += stat.size;
}
// Check rotated log files
for (let i = 1; i <= this.config.maxFiles; i++) {
const logFile = path.join(logDir, `${logBasename}.${i}${logExt}`);
if (await fs.pathExists(logFile)) {
const stat = await fs.stat(logFile);
stats.files.push({
file: logFile,
size: stat.size,
modified: stat.mtime
});
stats.totalSize += stat.size;
}
}
return stats;
} catch (error) {
this.error('Failed to get log stats', { error: error.message });
return { files: [], totalSize: 0 };
}
}
// Configuration updates
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
}
setLevel(level) {
if (!Object.prototype.hasOwnProperty.call(this.levels, level)) {
throw new Error(`Invalid log level: ${level}`);
}
this.config.level = level;
}
setSilent(silent = true) {
this.config.silent = silent;
}
setColorize(colorize = true) {
this.config.colorize = colorize;
}
setJson(json = true) {
this.config.json = json;
}
}
module.exports = Logger;