110 lines
3.0 KiB
JavaScript
110 lines
3.0 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
class CsvLogger {
|
|
/**
|
|
* @param {string} logDir - Directory where log files will be saved
|
|
* @param {string[]} headers - CSV header row
|
|
* @param {number} maxFileAgeDays - How many days log files should be kept
|
|
*/
|
|
constructor(logDir, headers = ['timestamp', 'level', 'message'], maxFileAgeDays = 30) {
|
|
this.logDir = logDir;
|
|
this.headers = headers;
|
|
this.maxFileAgeDays = maxFileAgeDays;
|
|
this.timestamp = null
|
|
|
|
if (!fs.existsSync(logDir)) {
|
|
fs.mkdirSync(logDir, { recursive: true });
|
|
}
|
|
|
|
// Cleanup old log files once on startup
|
|
this.deleteOldLogFiles().catch(console.error);
|
|
}
|
|
|
|
/**
|
|
* Writes a new log entry to today's CSV file
|
|
* @param {string} level - Log level (e.g., 'info', 'error')
|
|
* @param {string} message - Log message
|
|
* @param {string[]} extraFields - Optional additional fields
|
|
*/
|
|
log(level, message, extraFields = []) {
|
|
const timestamp = new Date().toISOString();
|
|
const filePath = this.getLogFilePath();
|
|
|
|
this.ensureFileWithHeaders(filePath);
|
|
|
|
const row = [timestamp, level, message, ...extraFields]
|
|
.map(this.escapeCsv)
|
|
.join(',') + '\n';
|
|
|
|
fs.appendFileSync(filePath, row, (err) => {
|
|
if (err) {
|
|
console.error('Failed to write to log file:', err);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Constructs the log file path for today
|
|
*/
|
|
getLogFilePath() {
|
|
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
let filename = "logs.csv"
|
|
|
|
if (this.timestamp == null) {
|
|
filename = `logs_${today}.csv`
|
|
} else {
|
|
filename = `${this.timestamp}_logs.csv`
|
|
}
|
|
return path.join(this.logDir, filename);
|
|
}
|
|
|
|
/**
|
|
* Ensures headers are written if file doesn't exist
|
|
*/
|
|
ensureFileWithHeaders(filePath) {
|
|
if (!fs.existsSync(filePath)) {
|
|
const headerRow = this.headers.join(',') + '\n';
|
|
fs.writeFileSync(filePath, headerRow);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Escapes a value for safe CSV output
|
|
*/
|
|
escapeCsv(value) {
|
|
if (value == null) return '';
|
|
const str = String(value);
|
|
return /[",\n]/.test(str) ? `"${str.replace(/"/g, '""')}"` : str;
|
|
}
|
|
|
|
/**
|
|
* Deletes log files older than maxFileAgeDays
|
|
*/
|
|
async deleteOldLogFiles() {
|
|
const files = await fs.promises.readdir(this.logDir);
|
|
|
|
const now = Date.now();
|
|
const maxAgeMs = this.maxFileAgeDays * 24 * 60 * 60 * 1000;
|
|
|
|
for (const file of files) {
|
|
const filePath = path.join(this.logDir, file);
|
|
if (!/^logs_\d{4}-\d{2}-\d{2}\.csv$/.test(file)) continue;
|
|
|
|
try {
|
|
const stats = await fs.promises.stat(filePath);
|
|
const age = now - stats.mtimeMs;
|
|
|
|
if (age > maxAgeMs) {
|
|
await fs.promises.unlink(filePath);
|
|
console.log(`Deleted old log file: ${file}`);
|
|
}
|
|
} catch (err) {
|
|
console.warn(`Could not process file ${file}: ${err.message}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = CsvLogger;
|