The server wrote gigabytes to ~/.kicad-mcp/logs and ignored LOG_LEVEL. Three root causes, all fixed here (the logging carve-out of #182): - Python (kicad_interface.py): replace the unbounded FileHandler with a RotatingFileHandler (10 MB x 3 backups, env-tunable via KICAD_MCP_LOG_MAX_BYTES / KICAD_MCP_LOG_BACKUP_COUNT); read the level from KICAD_MCP_LOG_LEVEL or LOG_LEVEL (default INFO) instead of hardcoding DEBUG; mute the noisy skip / skip.sexp.* loggers to WARNING unless KICAD_MCP_DEBUG_SKIP is set. - TypeScript (config.ts): honor KICAD_MCP_LOG_LEVEL / LOG_LEVEL for the TS logger. - TypeScript (logger.ts): size-cap the per-day log files with the same env knobs. - Docs + a no-network test for the env helpers, skip muting, and that no unbounded handler targets kicad_interface.log. Verified: LOG_LEVEL is now applied, skip is muted, and the file rotates instead of growing forever. Full suite unchanged from baseline. The hierarchical-sheet rewrite from #182 is intentionally left out (stays as a separate PR pending the #169/#170 design discussion). Co-Authored-By: angelorodem <angelorodem@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
153 lines
4.4 KiB
TypeScript
153 lines
4.4 KiB
TypeScript
/**
|
|
* Logger for KiCAD MCP server
|
|
*/
|
|
|
|
import { existsSync, mkdirSync, appendFileSync, statSync, renameSync, rmSync } from "fs";
|
|
import { join } from "path";
|
|
import * as os from "os";
|
|
|
|
// Log levels
|
|
type LogLevel = "error" | "warn" | "info" | "debug";
|
|
|
|
// Default log directory
|
|
const DEFAULT_LOG_DIR = join(os.homedir(), ".kicad-mcp", "logs");
|
|
|
|
/** Parse a non-negative integer env var, or fall back to a default. */
|
|
function envInt(name: string, fallback: number): number {
|
|
const raw = process.env[name];
|
|
if (raw === undefined) return fallback;
|
|
const value = Number.parseInt(raw, 10);
|
|
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
}
|
|
|
|
/**
|
|
* Logger class for KiCAD MCP server
|
|
*/
|
|
class Logger {
|
|
private logLevel: LogLevel = "info";
|
|
private logDir: string = DEFAULT_LOG_DIR;
|
|
// Size cap for the per-day log files (issue #181). Same env knobs as the
|
|
// Python side; KICAD_MCP_LOG_MAX_BYTES=0 disables rotation.
|
|
private maxBytes: number = envInt("KICAD_MCP_LOG_MAX_BYTES", 10 * 1024 * 1024);
|
|
private backupCount: number = envInt("KICAD_MCP_LOG_BACKUP_COUNT", 3);
|
|
|
|
/**
|
|
* Rotate the day's log file when it exceeds maxBytes so it can't grow
|
|
* without bound. Best-effort — never throws (logging must not break).
|
|
*/
|
|
private rotateIfNeeded(logFile: string): void {
|
|
if (this.maxBytes <= 0) return;
|
|
try {
|
|
if (!existsSync(logFile) || statSync(logFile).size < this.maxBytes) return;
|
|
if (this.backupCount <= 0) {
|
|
rmSync(logFile, { force: true });
|
|
return;
|
|
}
|
|
// Drop the oldest, shift .k -> .k+1, then current -> .1
|
|
const oldest = `${logFile}.${this.backupCount}`;
|
|
if (existsSync(oldest)) rmSync(oldest, { force: true });
|
|
for (let i = this.backupCount - 1; i >= 1; i--) {
|
|
const src = `${logFile}.${i}`;
|
|
if (existsSync(src)) renameSync(src, `${logFile}.${i + 1}`);
|
|
}
|
|
renameSync(logFile, `${logFile}.1`);
|
|
} catch {
|
|
// best-effort: if rotation fails, keep logging to the existing file
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the log level
|
|
* @param level Log level to set
|
|
*/
|
|
setLogLevel(level: LogLevel): void {
|
|
this.logLevel = level;
|
|
}
|
|
|
|
/**
|
|
* Set the log directory
|
|
* @param dir Directory to store log files
|
|
*/
|
|
setLogDir(dir: string): void {
|
|
this.logDir = dir;
|
|
|
|
// Ensure log directory exists
|
|
if (!existsSync(this.logDir)) {
|
|
mkdirSync(this.logDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log an error message
|
|
* @param message Message to log
|
|
*/
|
|
error(message: string): void {
|
|
this.log("error", message);
|
|
}
|
|
|
|
/**
|
|
* Log a warning message
|
|
* @param message Message to log
|
|
*/
|
|
warn(message: string): void {
|
|
if (["error", "warn", "info", "debug"].includes(this.logLevel)) {
|
|
this.log("warn", message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log an info message
|
|
* @param message Message to log
|
|
*/
|
|
info(message: string): void {
|
|
if (["info", "debug"].includes(this.logLevel)) {
|
|
this.log("info", message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log a debug message
|
|
* @param message Message to log
|
|
*/
|
|
debug(message: string): void {
|
|
if (this.logLevel === "debug") {
|
|
this.log("debug", message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log a message with the specified level
|
|
* @param level Log level
|
|
* @param message Message to log
|
|
*/
|
|
private log(level: LogLevel, message: string): void {
|
|
const now = new Date();
|
|
const pad = (n: number, w = 2) => String(n).padStart(w, "0");
|
|
const timestamp =
|
|
`${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` +
|
|
`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())},${pad(now.getMilliseconds(), 3)}`;
|
|
const formattedMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
|
|
|
|
// Log to console.error (stderr) only - stdout is reserved for MCP protocol
|
|
// All log levels go to stderr to avoid corrupting STDIO MCP transport
|
|
console.error(formattedMessage);
|
|
|
|
// Log to file
|
|
try {
|
|
// Ensure log directory exists
|
|
if (!existsSync(this.logDir)) {
|
|
mkdirSync(this.logDir, { recursive: true });
|
|
}
|
|
|
|
const logFile = join(this.logDir, `kicad-mcp-${new Date().toISOString().split("T")[0]}.log`);
|
|
this.rotateIfNeeded(logFile);
|
|
appendFileSync(logFile, formattedMessage + "\n");
|
|
} catch (error) {
|
|
console.error(`Failed to write to log file: ${error}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create and export logger instance
|
|
export const logger = new Logger();
|