logger.ts•2.31 kB
import fs from "fs/promises";
import path from "path";
import { chmodSync } from "fs";
export type LoggerOptions = {
logPath?: string;
truncateOnStart?: boolean;
maxSizeBytes?: number;
maxBackups?: number;
};
export class Logger {
private logPath: string;
private maxSize: number;
private maxBackups: number;
private constructor(logPath: string, maxSize: number, maxBackups: number) {
this.logPath = logPath;
this.maxSize = maxSize;
this.maxBackups = maxBackups;
}
static async create(opts: LoggerOptions = {}) {
const home = process.env.HOME || process.env.USERPROFILE || ".";
const dir = path.join(home, ".mcp-spotify");
await fs.mkdir(dir, { recursive: true });
const logPath = opts.logPath ?? path.join(dir, "mcp-spotify.log");
const maxSize = opts.maxSizeBytes ?? 5 * 1024 * 1024; // 5MB
const maxBackups = opts.maxBackups ?? 3;
const truncateOnStart = opts.truncateOnStart ?? true;
if (truncateOnStart) {
await fs.writeFile(logPath, "");
try {
chmodSync(logPath, 0o600);
} catch {}
}
return new Logger(logPath, maxSize, maxBackups);
}
private async rotateIfNeeded() {
try {
const st = await fs.stat(this.logPath);
if (st.size < this.maxSize) return;
// rotate files: log -> log.1, log.1 -> log.2, etc.
for (let i = this.maxBackups - 1; i >= 1; i--) {
const from = `${this.logPath}.${i}`;
const to = `${this.logPath}.${i + 1}`;
try {
await fs.rename(from, to);
} catch {}
}
try {
await fs.rename(this.logPath, `${this.logPath}.1`);
} catch {}
await fs.writeFile(this.logPath, "");
} catch {
// ignore stat errors
}
}
private async write(level: string, msg: string) {
const line = `${new Date().toISOString()} [${level}] ${msg}\n`;
try {
await fs.appendFile(this.logPath, line);
await this.rotateIfNeeded();
} catch {
// ignore file write errors
}
// also mirror to stderr so stdio-based host sees logs
console.error(line.trim());
}
info(msg: string) {
return this.write("INFO", msg);
}
error(msg: string) {
return this.write("ERROR", msg);
}
debug(msg: string) {
return this.write("DEBUG", msg);
}
}