ludus-mcp
Version:
MCP server for managing Ludus cybersecurity training environments through natural language commands
136 lines • 5.03 kB
JavaScript
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
export class FileLogger {
constructor(logName = 'ludus-mcp', maxFileSize = 10 * 1024 * 1024, // 10MB
maxFiles = 5) {
// Create logs directory in user's temp/home directory
this.logDir = path.join(os.homedir(), '.ludus-mcp', 'logs');
this.logFile = path.join(this.logDir, `${logName}.log`);
this.maxFileSize = maxFileSize;
this.maxFiles = maxFiles;
this.ensureLogDirectory();
}
ensureLogDirectory() {
try {
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
}
catch (error) {
console.error('Failed to create log directory:', error);
}
}
rotateIfNeeded() {
try {
if (!fs.existsSync(this.logFile)) {
return;
}
const stats = fs.statSync(this.logFile);
if (stats.size >= this.maxFileSize) {
this.rotateFiles();
}
}
catch (error) {
console.error('Error checking log file size:', error);
}
}
rotateFiles() {
try {
// Move current log to .1, .1 to .2, etc.
for (let i = this.maxFiles - 1; i >= 1; i--) {
const oldFile = `${this.logFile}.${i}`;
const newFile = `${this.logFile}.${i + 1}`;
if (fs.existsSync(oldFile)) {
if (i === this.maxFiles - 1) {
fs.unlinkSync(oldFile); // Delete oldest
}
else {
fs.renameSync(oldFile, newFile);
}
}
}
// Move current log to .1
if (fs.existsSync(this.logFile)) {
fs.renameSync(this.logFile, `${this.logFile}.1`);
}
}
catch (error) {
console.error('Error rotating log files:', error);
}
}
formatValue(value) {
if (value === null)
return 'null';
if (value === undefined)
return 'undefined';
if (typeof value === 'string')
return value;
if (typeof value === 'number' || typeof value === 'boolean')
return String(value);
// Handle Error objects specially
if (value instanceof Error) {
return `${value.name}: ${value.message}\n${value.stack || 'No stack trace'}`;
}
// Handle objects that might have circular references or non-enumerable properties
try {
// First try regular JSON.stringify
return JSON.stringify(value, null, 2);
}
catch (jsonError) {
// Fallback: try to extract common error properties manually
if (value && typeof value === 'object') {
const errorProps = [];
// Try to get common error properties
const commonProps = ['message', 'code', 'errno', 'syscall', 'path', 'name', 'stack', 'signal', 'status', 'stderr', 'stdout'];
for (const prop of commonProps) {
if (prop in value && value[prop] !== undefined) {
errorProps.push(`${prop}: ${String(value[prop])}`);
}
}
// If we found some properties, use them
if (errorProps.length > 0) {
return errorProps.join('\n');
}
// Last resort: try to get all enumerable properties
try {
const props = Object.keys(value).map(key => `${key}: ${String(value[key])}`);
if (props.length > 0) {
return props.join('\n');
}
}
catch (e) {
// Even this failed
}
}
// Final fallback
return `[Complex object - toString: ${String(value)}]`;
}
}
log(level, context, message, meta) {
try {
this.rotateIfNeeded();
const timestamp = new Date().toISOString();
let logLine = `[${timestamp}] ${level.toUpperCase()} [${context}] ${message}`;
if (meta && Object.keys(meta).length > 0) {
logLine += '\n Metadata:';
for (const [key, value] of Object.entries(meta)) {
const formattedValue = this.formatValue(value);
logLine += `\n ${key}: ${formattedValue}`;
}
}
logLine += '\n\n';
fs.appendFileSync(this.logFile, logLine, 'utf8');
}
catch (error) {
console.error('Failed to write to log file:', error);
}
}
getLogPath() {
return this.logFile;
}
getLogDirectory() {
return this.logDir;
}
}
//# sourceMappingURL=fileLogger.js.map