Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions emain/log.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,107 @@
// 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";
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" }),
];
Expand All @@ -23,6 +117,7 @@ const loggerConfig = {
transports: loggerTransports,
};
const logger = winston.createLogger(loggerConfig);

function log(...msg: any[]) {
try {
logger.info(format(...msg));
Expand All @@ -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);
}
Comment on lines +129 to +140
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore error-level logging for rotation failures.

logRotateError and pruneError used to be emitted through logger.error, but now they go through log(...), which always logs at INFO. That masks actual failures from error-level monitors/alerts. Please keep these cases at ERROR while still formatting the message inline.

-if (logRotateError != null) {
-    log("error rotating/pruning logs (non-fatal):", logRotateError);
-}
+if (logRotateError != null) {
+    logger.error(format("error rotating/pruning logs (non-fatal): %o", logRotateError));
+}
 ...
-if (pruneError != null) {
-    log("error pruning some log files (non-fatal):", pruneError);
-}
+if (pruneError != null) {
+    logger.error(format("error pruning some log files (non-fatal): %o", pruneError));
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
if (logRotateError != null) {
logger.error(format("error rotating/pruning logs (non-fatal): %o", 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) {
logger.error(format("error pruning some log files (non-fatal): %o", pruneError));
}
🤖 Prompt for AI Agents
In emain/log.ts around lines 129 to 140, error cases for logRotateError and
pruneError are being emitted via log(...) (INFO) instead of logger.error; change
those two branches to call logger.error and include the formatted inline message
plus the error object (e.g. logger.error("error rotating/pruning logs
(non-fatal): %s", String(logRotateError), logRotateError)) so the error-level
monitor receives the event and the error details/stack are preserved; keep the
rotatedPath and prunedFiles branches using log(...) as-is.


export { log };
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading