API/Client-Server/js/core/Logger.js

305 lines
10 KiB
JavaScript

/**
* Galaxy Strike Online - Logging System
* Provides file-based logging with rotation and formatting
* Renderer process version that uses IPC to communicate with main process
*/
class Logger {
constructor() {
this.logLevel = 'INFO';
this.isRenderer = typeof window !== 'undefined' && typeof window.electronAPI !== 'undefined';
this.timers = new Map();
this.levels = {
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3
};
}
async initialize(appDataPath) {
if (this.isRenderer) {
// In renderer process, just log that we're ready
console.log('Logger initialized in renderer process');
return;
}
// Main process initialization (original code)
const fs = require('fs').promises;
const path = require('path');
try {
// Set up log directory in app storage location
this.logDir = path.join(appDataPath, 'logs');
// Create logs directory if it doesn't exist
await fs.mkdir(this.logDir, { recursive: true });
// Set current log file with full timestamp (YYYY-MM-DD-HH-MM-SS)
const now = new Date();
const timestamp = now.toISOString()
.replace(/T/, '-')
.replace(/\..+/, '')
.replace(/:/g, '-');
this.currentLogFile = path.join(this.logDir, `galaxy-strike-${timestamp}.log`);
// Test write to ensure permissions
await this.writeToFile('=== Galaxy Strike Online Log Session Started ===\n');
this.isInitialized = true;
console.log(`Logger initialized: ${this.logDir}`);
} catch (error) {
console.error('Failed to initialize logger:', error);
this.isInitialized = false;
}
}
async writeToFile(message) {
if (this.isRenderer) {
// In renderer process, send to main process via IPC
return;
}
if (!this.isInitialized || !this.currentLogFile) return;
try {
const fs = require('fs').promises;
// Check if file needs rotation
await this.rotateLogIfNeeded();
// Append message to current log file
await fs.appendFile(this.currentLogFile, message, 'utf8');
} catch (error) {
console.error('Failed to write to log file:', error);
}
}
async rotateLogIfNeeded() {
if (this.isRenderer) return;
const fs = require('fs').promises;
const path = require('path');
try {
const stats = await fs.stat(this.currentLogFile);
if (stats.size >= this.maxFileSize) {
// Rotate log file with full timestamp
const now = new Date();
const timestamp = now.toISOString()
.replace(/T/, '-')
.replace(/\..+/, '')
.replace(/:/g, '-');
const rotatedFile = path.join(this.logDir, `galaxy-strike-${timestamp}.log`);
await fs.rename(this.currentLogFile, rotatedFile);
// Clean up old log files
await this.cleanupOldLogs();
// Create new current log file with new timestamp
const newTimestamp = new Date().toISOString()
.replace(/T/, '-')
.replace(/\..+/, '')
.replace(/:/g, '-');
this.currentLogFile = path.join(this.logDir, `galaxy-strike-${newTimestamp}.log`);
await this.writeToFile('=== Log Rotated ===\n');
}
} catch (error) {
// File might not exist yet, which is fine
if (error.code !== 'ENOENT') {
console.error('Failed to rotate log:', error);
}
}
}
async cleanupOldLogs() {
if (this.isRenderer) return;
const fs = require('fs').promises;
const path = require('path');
try {
const files = await fs.readdir(this.logDir);
const logFiles = files
.filter(file => file.startsWith('galaxy-strike-') && file.endsWith('.log'))
.map(file => ({
name: file,
path: path.join(this.logDir, file)
}));
if (logFiles.length > this.maxLogFiles) {
// Get file stats and sort by modification time
const filesWithStats = await Promise.all(
logFiles.map(async file => {
const stats = await fs.stat(file.path);
return { ...file, mtime: stats.mtime };
})
);
filesWithStats.sort((a, b) => b.mtime - a.mtime);
// Delete oldest files
const filesToDelete = filesWithStats.slice(this.maxLogFiles);
for (const file of filesToDelete) {
await fs.unlink(file.path);
}
}
} catch (error) {
console.error('Failed to cleanup old logs:', error);
}
}
formatMessage(level, message, data = null) {
const timestamp = new Date().toISOString();
const dataStr = data ? ` | Data: ${JSON.stringify(data)}` : '';
return `[${timestamp}] [${level}] ${message}${dataStr}\n`;
}
shouldLog(level) {
const currentLevel = this.levels[this.logLevel] || 2;
const messageLevel = this.levels[level] || 2;
return messageLevel <= currentLevel;
}
async log(level, message, data = null) {
if (!this.shouldLog(level)) return;
if (this.isRenderer && window.electronAPI) {
// Send to main process via IPC - ensure data is serializable
try {
const serializableData = data ? this.makeSerializable(data) : null;
window.electronAPI.log(level, message, serializableData);
} catch (error) {
console.error('Failed to send log to main process:', error);
// Fallback to console
console.log(`[${level}] ${message}`, data || '');
}
} else {
// Main process logging
const formattedMessage = this.formatMessage(level, message, data);
await this.writeToFile(formattedMessage);
console.log(`[${level}] ${message}`, data || '');
}
}
makeSerializable(obj) {
try {
// Convert to JSON and back to ensure it's serializable
return JSON.parse(JSON.stringify(obj));
} catch (error) {
// If not serializable, convert to string representation
return {
type: 'non-serializable',
toString: obj.toString ? obj.toString() : String(obj),
constructor: obj.constructor ? obj.constructor.name : 'Unknown'
};
}
}
async error(message, data = null) {
await this.log('error', message, data);
console.error(`[ERROR] ${message}`, data || '');
}
async warn(message, data = null) {
await this.log('warn', message, data);
console.warn(`[WARN] ${message}`, data || '');
}
async info(message, data = null) {
await this.log('info', message, data);
}
async debug(message, data = null) {
await this.log('debug', message, data);
}
async gameEvent(eventType, details) {
const message = `Game Event: ${eventType}`;
await this.info(message, details);
}
// Timing methods
startTimer(name) {
this.timers.set(name, performance.now());
}
endTimer(name) {
const startTime = this.timers.get(name);
if (startTime) {
const duration = performance.now() - startTime;
this.timers.delete(name);
return duration;
}
return 0;
}
async timeAsync(name, asyncFunction) {
this.startTimer(name);
try {
const result = await asyncFunction();
const duration = this.endTimer(name);
await this.info(`${name} completed`, { duration: `${duration.toFixed(2)}ms` });
return result;
} catch (error) {
const duration = this.endTimer(name);
await this.error(`${name} failed`, { duration: `${duration.toFixed(2)}ms`, error: error.message });
throw error;
}
}
async playerAction(action, details) {
const message = `Player Action: ${action}`;
await this.info(message, details);
}
async systemEvent(system, event, details) {
const message = `System Event: ${system} - ${event}`;
await this.info(message, details);
}
async errorEvent(error, context = null) {
const message = `Error Event: ${error.message || error}`;
const data = {
stack: error.stack,
context: context
};
await this.error(message, data);
}
setLogLevel(level) {
if (this.levels.hasOwnProperty(level)) {
this.logLevel = level;
this.info(`Log level changed to: ${level}`);
}
}
getLogInfo() {
return {
isInitialized: this.isInitialized,
isRenderer: this.isRenderer,
logDirectory: this.logDir,
currentLogFile: this.currentLogFile,
logLevel: this.logLevel
};
}
}
// Export singleton instance
const logger = new Logger();
// For Node.js environments
if (typeof module !== 'undefined' && module.exports) {
module.exports = logger;
}
// For browser environments
if (typeof window !== 'undefined') {
window.logger = logger;
}