netakhoj-api / utils /logger.js
Rakeshops71
Deploy app with LFS for large files
3eedfc9
Raw
History Blame Contribute Delete
7.18 kB
// utils/logger.js - PRODUCTION LOGGER
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class Logger {
constructor(namespace, options = {}) {
this.namespace = namespace;
this.options = {
writeToFile: options.writeToFile ?? (process.env.LOG_TO_FILE === 'true' || process.env.NODE_ENV === 'production'),
logDir: options.logDir || path.join(__dirname, '..', 'logs'),
maxFileSize: options.maxFileSize || 10 * 1024 * 1024, // 10MB
maxFiles: options.maxFiles || 7, // Keep 7 days
consoleOutput: options.consoleOutput ?? true,
...options
};
this.colors = {
INFO: '\x1b[36m', // Cyan
ERROR: '\x1b[31m', // Red
WARN: '\x1b[33m', // Yellow
DEBUG: '\x1b[35m', // Magenta
SUCCESS: '\x1b[32m', // Green
REQUEST: '\x1b[34m', // Blue
RESET: '\x1b[0m',
};
this.icons = {
INFO: 'ℹ️',
ERROR: '❌',
WARN: '⚠️',
DEBUG: '🔍',
SUCCESS: '✅',
REQUEST: '🌐',
};
if (this.options.writeToFile) {
this._ensureLogDir();
this._cleanupOldLogs();
}
}
_ensureLogDir() {
if (!fs.existsSync(this.options.logDir)) {
fs.mkdirSync(this.options.logDir, { recursive: true });
}
}
_getLogFilePath(level) {
const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
const filename = level === 'REQUEST' ? 'requests' : level === 'ERROR' ? 'error' : 'combined';
return path.join(this.options.logDir, `${date}-${filename}.log`);
}
_rotateLogIfNeeded(filepath) {
if (!fs.existsSync(filepath)) return;
const stats = fs.statSync(filepath);
if (stats.size > this.options.maxFileSize) {
const timestamp = Date.now();
const rotatedPath = filepath.replace('.log', `.${timestamp}.log`);
fs.renameSync(filepath, rotatedPath);
console.log(`🔄 Log rotated: ${path.basename(rotatedPath)}`);
}
}
_writeToFile(level, logEntry) {
if (!this.options.writeToFile) return;
try {
const filepath = this._getLogFilePath(level);
this._rotateLogIfNeeded(filepath);
const logLine = JSON.stringify(logEntry) + '\n';
fs.appendFileSync(filepath, logLine, 'utf8');
} catch (error) {
console.error('Failed to write log:', error.message);
}
}
_formatConsoleMessage(level, requestId, message, data) {
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const color = this.colors[level] || '';
const icon = this.icons[level] || '';
const reset = this.colors.RESET;
const prefix = `${color}${icon} [${timestamp}] [${this.namespace}] [${requestId}]${reset}`;
let dataStr = '';
if (data) {
if (typeof data === 'object') {
const keys = Object.keys(data);
if (keys.length <= 3) {
dataStr = ' ' + JSON.stringify(data);
} else {
dataStr = '\n' + JSON.stringify(data, null, 2);
}
} else {
dataStr = ' ' + data;
}
}
return `${prefix} ${message}${dataStr}`;
}
_log(level, requestId, message, data) {
const timestamp = new Date().toISOString();
// Create structured log entry
const logEntry = {
timestamp,
level,
namespace: this.namespace,
requestId,
message,
...(data && { data }),
pid: process.pid,
memory: process.memoryUsage().heapUsed
};
// Console output
if (this.options.consoleOutput) {
const consoleMsg = this._formatConsoleMessage(level, requestId, message, data);
const logMethod = level === 'ERROR' ? console.error : console.log;
logMethod(consoleMsg);
}
// File output
this._writeToFile(level, logEntry);
}
info(requestId, message, data) {
this._log('INFO', requestId, message, data);
}
error(requestId, message, error) {
const errorData = error ? {
message: error?.message || error,
name: error?.name,
code: error?.code,
statusCode: error?.statusCode,
...(error?.stack ? { stack: error.stack.split('\n').slice(0, 5) } : {})
} : null;
this._log('ERROR', requestId, message, errorData);
}
warn(requestId, message, data) {
this._log('WARN', requestId, message, data);
}
debug(requestId, message, data) {
if (process.env.DEBUG === 'true') {
this._log('DEBUG', requestId, message, data);
}
}
success(requestId, message, data) {
this._log('SUCCESS', requestId, message, data);
}
request(requestId, message, params) {
const sanitized = { ...params };
// Don't log sensitive data
if (sanitized.meow) sanitized.meow = '***';
if (sanitized.bhaw) sanitized.bhaw = '***';
if (sanitized.apiKey) sanitized.apiKey = '***';
this._log('REQUEST', requestId, message, sanitized);
}
// Store response data to file
storeResponse(requestId, message, responseData) {
if (!this.options.writeToFile) return;
try {
const date = new Date().toISOString().split('T')[0];
const filepath = path.join(this.options.logDir, `${date}-prs-responses.log`);
this._rotateLogIfNeeded(filepath);
const logEntry = {
timestamp: new Date().toISOString(),
namespace: this.namespace,
requestId,
message,
responseData,
pid: process.pid
};
const logLine = JSON.stringify(logEntry, null, 2) + '\n---\n';
fs.appendFileSync(filepath, logLine, 'utf8');
} catch (error) {
console.error('Failed to store response:', error.message);
}
}
// Cleanup old logs
_cleanupOldLogs() {
try {
const files = fs.readdirSync(this.options.logDir);
const cutoffDate = Date.now() - (this.options.maxFiles * 24 * 60 * 60 * 1000);
let deleted = 0;
files.forEach(file => {
const filepath = path.join(this.options.logDir, file);
const stats = fs.statSync(filepath);
if (stats.mtimeMs < cutoffDate) {
fs.unlinkSync(filepath);
deleted++;
}
});
if (deleted > 0) {
console.log(`🧹 Log cleanup: Deleted ${deleted} old log files`);
}
} catch (error) {
console.error('Failed to cleanup logs:', error.message);
}
}
cleanupOldLogs() {
this._cleanupOldLogs();
}
getStats() {
try {
const files = fs.readdirSync(this.options.logDir);
let totalSize = 0;
files.forEach(file => {
const filepath = path.join(this.options.logDir, file);
totalSize += fs.statSync(filepath).size;
});
return {
logDir: this.options.logDir,
files: files.length,
totalSize: `${(totalSize / 1024 / 1024).toFixed(2)} MB`,
maxFileSize: `${(this.options.maxFileSize / 1024 / 1024).toFixed(2)} MB`,
maxFiles: this.options.maxFiles
};
} catch (error) {
return { error: error.message };
}
}
}
export const createLogger = (namespace, options) => new Logger(namespace, options);
export default new Logger('APP');