diff --git a/emain/log.ts b/emain/log.ts index 6e84ef0ab4..34fada4b36 100644 --- a/emain/log.ts +++ b/emain/log.ts @@ -1,6 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import fs from "fs"; import path from "path"; import { format } from "util"; import winston from "winston"; @@ -8,6 +9,99 @@ import { getWaveDataDir, isDev } from "./platform"; const oldConsoleLog = console.log; +function findHighestLogNumber(logsDir: string): number { + if (!fs.existsSync(logsDir)) { + return 0; + } + const files = fs.readdirSync(logsDir); + let maxNum = 0; + for (const file of files) { + const match = file.match(/^waveapp\.(\d+)\.log$/); + if (match) { + const num = parseInt(match[1], 10); + if (num > maxNum) { + maxNum = num; + } + } + } + return maxNum; +} + +function pruneOldLogs(logsDir: string): { pruned: string[]; error: any } { + if (!fs.existsSync(logsDir)) { + return { pruned: [], error: null }; + } + + const files = fs.readdirSync(logsDir); + const logFiles: { name: string; num: number }[] = []; + + for (const file of files) { + const match = file.match(/^waveapp\.(\d+)\.log$/); + if (match) { + logFiles.push({ name: file, num: parseInt(match[1], 10) }); + } + } + + if (logFiles.length <= 5) { + return { pruned: [], error: null }; + } + + logFiles.sort((a, b) => b.num - a.num); + const toDelete = logFiles.slice(5); + const pruned: string[] = []; + let firstError: any = null; + + for (const logFile of toDelete) { + try { + fs.unlinkSync(path.join(logsDir, logFile.name)); + pruned.push(logFile.name); + } catch (e) { + if (firstError == null) { + firstError = e; + } + } + } + + return { pruned, error: firstError }; +} + +function rotateLogIfNeeded(): string | null { + const waveDataDir = getWaveDataDir(); + const logFile = path.join(waveDataDir, "waveapp.log"); + const logsDir = path.join(waveDataDir, "logs"); + + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + + if (!fs.existsSync(logFile)) { + return null; + } + + const stats = fs.statSync(logFile); + if (stats.size > 10 * 1024 * 1024) { + const nextNum = findHighestLogNumber(logsDir) + 1; + const rotatedPath = path.join(logsDir, `waveapp.${nextNum}.log`); + fs.renameSync(logFile, rotatedPath); + return rotatedPath; + } + return null; +} + +let logRotateError: any = null; +let rotatedPath: string | null = null; +let prunedFiles: string[] = []; +let pruneError: any = null; +try { + rotatedPath = rotateLogIfNeeded(); + const logsDir = path.join(getWaveDataDir(), "logs"); + const pruneResult = pruneOldLogs(logsDir); + prunedFiles = pruneResult.pruned; + pruneError = pruneResult.error; +} catch (e) { + logRotateError = e; +} + const loggerTransports: winston.transport[] = [ new winston.transports.File({ filename: path.join(getWaveDataDir(), "waveapp.log"), level: "info" }), ]; @@ -23,6 +117,7 @@ const loggerConfig = { transports: loggerTransports, }; const logger = winston.createLogger(loggerConfig); + function log(...msg: any[]) { try { logger.info(format(...msg)); @@ -31,4 +126,17 @@ function log(...msg: any[]) { } } +if (logRotateError != null) { + log("error rotating/pruning logs (non-fatal):", logRotateError); +} +if (rotatedPath != null) { + log("rotated old log file to:", rotatedPath); +} +if (prunedFiles.length > 0) { + log("pruned old log files:", prunedFiles.join(", ")); +} +if (pruneError != null) { + log("error pruning some log files (non-fatal):", pruneError); +} + export { log }; diff --git a/package-lock.json b/package-lock.json index dc23c81a4d..b175b6dd22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "waveterm", - "version": "0.12.0-beta.1", + "version": "0.12.0-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "waveterm", - "version": "0.12.0-beta.1", + "version": "0.12.0-beta.2", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [