Log message content
Severity level of the log (default: INFO)
Whether to perform log rotation if needed (default: true)
Boolean indicating success or failure
export function saveLog(
message: string,
level: LogLevel = "INFO",
allowRotation = true,
): boolean {
try {
// Create logs directory if it doesn't exist
if (!fs.existsSync(logsPath)) {
fs.mkdirSync(logsPath, { recursive: true });
}
// Get current settings (especially log level filter)
const settings = getSettings();
const logLevelOrder = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"];
const configuredLogLevel =
settings.fileLogLevel || settings.logLevel || "INFO";
const configuredLevelIndex = logLevelOrder.indexOf(configuredLogLevel);
const currentLevelIndex = logLevelOrder.indexOf(level);
// Skip logs below the configured log level
if (currentLevelIndex < configuredLevelIndex) {
return false;
}
// Format timestamp HH:MM:SS AM/PM.ms format
const now = new Date();
const formattedTime =
now
.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
})
.replace(/\s/, " ") +
`.${now.getMilliseconds().toString().padStart(3, "0")}`;
const logLine = `[${formattedTime}] [${level}] ${message}\n`;
// Create a key for deduplication (exclude timestamp for comparing content)
const dedupeKey = `[${level}] ${message}`;
const timestamp = now.getTime();
// Check for duplicate logs within the deduplication window
const existingLog = recentLogs.get(dedupeKey);
if (existingLog) {
// If the log is identical and recent, just increment the count
if (timestamp - existingLog.timestamp < DEDUPLICATION_WINDOW_MS) {
existingLog.count++;
existingLog.timestamp = timestamp;
// Don't write duplicate logs
return true;
}
// If we've accumulated duplicates but it's been long enough, write a summary
if (existingLog.count > 1) {
const summaryLine = `[${formattedTime}] [${level}] Last message repeated ${existingLog.count} times\n`;
fs.appendFileSync(latestLogPath, summaryLine, { encoding: "utf-8" });
}
}
// Update the recent logs cache
recentLogs.set(dedupeKey, { count: 1, timestamp });
// Clean up old entries from the deduplication cache (older than the window)
for (const [key, entry] of recentLogs.entries()) {
if (timestamp - entry.timestamp > DEDUPLICATION_WINDOW_MS) {
recentLogs.delete(key);
}
}
// Append log to file
fs.appendFileSync(latestLogPath, logLine, { encoding: "utf-8" });
// Perform log rotation if needed
if (allowRotation) {
const settings = getSettings();
const MAX_LOG_LINES = settings.logLineCount || 1000;
if (fs.existsSync(latestLogPath)) {
const logContent = fs.readFileSync(latestLogPath, "utf-8");
const logLines = logContent
.split("\n")
.filter((line) => line.trim() !== "");
if (logLines.length > MAX_LOG_LINES) {
// Keep only the most recent MAX_LOG_LINES lines
const truncatedLines = logLines.slice(-MAX_LOG_LINES);
fs.writeFileSync(
latestLogPath,
truncatedLines.join("\n") + "\n",
"utf-8",
);
console.log(
`Log file rotated, kept ${MAX_LOG_LINES} most recent lines`,
);
// Also log this rotation event
saveLog(
`Log file rotated, keeping ${MAX_LOG_LINES} most recent lines`,
"INFO",
false,
);
// Check and enforce the maximum number of log files
if (settings.maxLogFiles && settings.maxLogFiles > 0) {
cleanupOldLogs(settings.maxLogFiles);
}
}
}
}
return true;
} catch (error) {
console.error("Failed to save log:", error);
return false;
}
}
Saves a log message to the application log file