import fs from 'fs'; import path from 'path'; // 로그 디렉토리 생성 const logDir = './logs'; if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } // 로그 레벨 정의 const logLevels = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, CRITICAL: 4 }; const logLevelNames = Object.keys(logLevels); const currentLogLevel = logLevels.info; // 설정에 따라 변경 가능 // 타임스탬프 생성 const getTimestamp = () => new Date().toISOString(); // 호출자 정보 추출 (ESM 호환) const getCallerInfo = () => { const err = new Error(); const stack = err.stack.split('\n'); // 스택에서 호출자 위치 찾기 let callerIndex = 2; // 기본값 (0: Error, 1: getCallerInfo, 2: logger function) // 'at '로 시작하는 라인 찾기 for (let i = 2; i < stack.length; i++) { if (stack[i].includes('at ')) { callerIndex = i; break; } } if (stack.length > callerIndex) { const callerLine = stack[callerIndex].trim(); // 파일 경로 추출 (Windows와 Unix 경로 모두 처리) const fileMatch = callerLine.match(/\((.*?):\d+:\d+\)/) || callerLine.match(/(.*?):\d+:\d+$/); if (fileMatch && fileMatch[1]) { const filePath = fileMatch[1]; const fileName = path.basename(filePath); return fileName; } } return 'unknown'; }; // 로그 포맷팅 const formatLog = (level, message, metadata = {}) => { const timestamp = getTimestamp(); const callerInfo = getCallerInfo(); const levelName = logLevelNames[level]; let logMessage = `[${levelName}] ${timestamp} [${callerInfo}] ${message}`; if (Object.keys(metadata).length > 0) { try { logMessage += `\n Meta: ${JSON.stringify(metadata, null, 2)}`; } catch (e) { logMessage += `\n Meta: [Circular or invalid object]`; } } return logMessage; }; // 로그 파일 쓰기 const writeLog = (logMessage, level) => { if (level < currentLogLevel) return; // 콘솔 출력 if (level >= logLevels.ERROR) { console.error(logMessage); } else { console.log(logMessage); } // 파일 로깅 try { const date = new Date().toISOString().split('T')[0]; const logFile = path.join(logDir, `trading-${date}.log`); fs.appendFileSync(logFile, logMessage + '\n'); } catch (e) { // 파일 로깅 실패 시 콘솔에 경고 console.error(`[LOGGER] Failed to write log file: ${e.message}`); } }; // 로거 인스턴스 생성 const createLogger = () => { return { debug: (message, metadata = {}) => { const logMessage = formatLog(logLevels.DEBUG, message, metadata); writeLog(logMessage, logLevels.DEBUG); }, info: (message, metadata = {}) => { const logMessage = formatLog(logLevels.INFO, message, metadata); writeLog(logMessage, logLevels.INFO); }, warn: (message, metadata = {}) => { const logMessage = formatLog(logLevels.WARN, message, metadata); writeLog(logMessage, logLevels.WARN); }, error: (message, error = null, metadata = {}) => { const errorMetadata = { ...metadata }; if (error) { errorMetadata.error = { message: error.message || 'Unknown error', stack: error.stack ? error.stack.split('\n').slice(0, 5).join('\n') : 'No stack trace' }; } const logMessage = formatLog(logLevels.ERROR, message, errorMetadata); writeLog(logMessage, logLevels.ERROR); }, critical: (message, error = null, metadata = {}) => { const errorMetadata = { ...metadata }; if (error) { errorMetadata.error = { message: error.message || 'Unknown critical error', stack: error.stack || 'No stack trace' }; } const logMessage = formatLog(logLevels.CRITICAL, message, errorMetadata); writeLog(logMessage, logLevels.CRITICAL); }, logPosition: (position, message) => { if (!position) return; const metadata = { position: { id: position.id, side: position.side, entry: position.entry, qty: position.qty, sl: position.sl, tp: position.tp, openTime: position.openTime ? new Date(position.openTime).toISOString() : 'N/A' } }; if (position.closed) { metadata.position.exitPrice = position.exitPrice; metadata.position.exitReason = position.exitReason; metadata.position.pnl = position.pnl; metadata.position.closeTime = position.closeTime ? new Date(position.closeTime).toISOString() : 'N/A'; } logger.info(message, metadata); } }; }; // 싱글톤 인스턴스 생성 const logger = createLogger(); // 모듈 내보내기 export default logger;