UNPKG

@homecheck/logger

Version:

A simple logger for Web, Node, Capacitor apps.

370 lines (369 loc) 14.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LogLevel = exports.Logger = void 0; const types_1 = require("./types"); const utils_1 = require("./utils"); const storage_1 = require("./storage"); const network_1 = require("./network"); class Logger { constructor(options) { this.networkStatus = { online: true, lastChecked: 0 }; this.processingQueue = false; this.retryTimeoutId = null; this.maxRetries = 3; this.retryCount = 0; this.utc = options.utc; this.reportServers = options.reportServers || {}; this.projectName = options.projectName; this.maxQueueSize = options.maxQueueSize || 1000; this.batchSize = options.batchSize || 50; this.retryInterval = options.retryInterval || 30000; // 30초 this.storage = (0, storage_1.createStorage)(); // 스토리지 초기화 this.storage.init().then(() => { // 초기화 후 큐 처리 시작 this.processQueue(); }).catch(err => { // 에러 메시지에 [Logger] 접두사 추가 console.error(`[Logger] Failed to initialize logger storage: ${err}`); }); // 네트워크 상태 모니터링 설정 this.setupNetworkMonitoring(); } static initialize(options) { if (!Logger.instance) Logger.instance = new Logger(options); return Logger.instance; } static getInstance() { this.ensureInitialized(); return this.instance; } setupNetworkMonitoring() { // 브라우저 환경에서 온라인/오프라인 이벤트 리스너 설정 if (typeof window !== 'undefined') { window.addEventListener('online', () => { this.networkStatus.online = true; this.networkStatus.lastChecked = Date.now(); this.processQueue(); }); window.addEventListener('offline', () => { this.networkStatus.online = false; this.networkStatus.lastChecked = Date.now(); }); } } // 기본 로그 함수 createLog(data, level) { const timestamp = (0, utils_1.getFormattedTimestamp)(this.utc); const stack = (0, utils_1.getStackTrace)(this.projectName, data instanceof Error ? 1 : 3); const osInfo = (0, utils_1.getOsInfo)(); // 데이터 -> 문자열 const text = (0, utils_1.formatDataToString)(data); let log = undefined; // Error 객체인 경우 스택 트레이스 포함 if (data instanceof Error && data.stack) log = data.stack; const logData = { ...stack, text, timestamp, os: osInfo, level, log }; // 콘솔에 출력할 문자열 생성 const colorLogString = (0, utils_1.formatLogString)(logData, false); console.log(colorLogString); return { // 서버 전송을 처리하는 함수 반환 report: async (serverNames) => { try { await this.queueLog(logData, serverNames); } catch (err) { console.error(`[Logger] Failed to report log: ${err}`); } } }; } // 로그를 큐에 추가하고 처리 시도 async queueLog(logData, serverNames) { try { // 큐 크기 확인 const count = await this.storage.getCount(); if (count >= this.maxQueueSize) { // 큐가 가득 찼을 경우 가장 오래된 로그 제거 const oldestLogs = await this.storage.getLogs(1); if (oldestLogs.length > 0) await this.storage.removeLogs([oldestLogs[0].id]); } // 서버 이름 정보 추가 const logWithServers = { ...logData, serverNames }; // 로그 저장 await this.storage.addLog(logWithServers); // 온라인 상태이면 큐 처리 시도 if (this.checkNetworkStatus()) this.processQueue(); } catch (error) { throw new Error(`Failed to queue log: ${error}`); } } // 네트워크 상태 확인 checkNetworkStatus() { const now = Date.now(); // 마지막 확인 후 1분 이상 지났으면 다시 확인 if (now - this.networkStatus.lastChecked > 60000) { this.networkStatus.online = (0, network_1.isOnline)(); this.networkStatus.lastChecked = now; } return this.networkStatus.online; } // 큐에 있는 로그 처리 async processQueue() { // 이미 처리 중이면 중복 실행 방지 if (this.processingQueue) return; try { this.processingQueue = true; // 네트워크 상태 확인 if (!this.checkNetworkStatus()) { this.scheduleRetry(); return; } // 큐에서 로그 가져오기 const count = await this.storage.getCount(); if (count === 0) { this.processingQueue = false; return; } // 배치 크기만큼 로그 가져오기 const logs = await this.storage.getLogs(this.batchSize); if (logs.length === 0) { this.processingQueue = false; return; } // 서버별로 로그 그룹화 const serverGroups = this.groupLogsByServer(logs); // 각 서버로 로그 전송 const results = await Promise.allSettled(Object.entries(serverGroups).map(async ([serverName, serverLogs]) => { const server = this.reportServers[serverName]; if (!server) return; // 서버 연결 가능 여부 확인 const reachable = await (0, network_1.checkServerReachable)(server.url); if (!reachable) return; // 인증 헤더 설정 const headers = { 'Content-Type': 'application/json' }; if (server.token) headers['Authorization'] = `Bearer ${server.token}`; try { // 배치 모드인 경우 한 번에 전송 if (server.batch) { await this.sendBatchLogs(server, serverLogs, headers); } else { // 단건 모드인 경우 개별 전송 for (const log of serverLogs) await this.sendSingleLog(server, log, headers); } return serverLogs.map(log => log.id); } catch (error) { throw new Error(`Failed to send logs to server ${serverName}: ${error}`); } })); // 성공적으로 전송된 로그 ID 수집 const successIds = results .filter(result => result.status === 'fulfilled' && result.value) .flatMap(result => result.value || []); // 전송 성공한 로그 삭제 if (successIds.length > 0) await this.storage.removeLogs(successIds); // 재시도 카운터 초기화 this.retryCount = 0; // 남은 로그가 있으면 계속 처리 const remainingCount = await this.storage.getCount(); if (remainingCount > 0) this.processQueue(); else this.processingQueue = false; } catch (error) { console.error(`[Logger] Error processing log queue: ${error}`); // 재시도 횟수 증가 this.retryCount++; // 최대 재시도 횟수를 초과하지 않았으면 재시도 if (this.retryCount < this.maxRetries) this.scheduleRetry(); else { // 최대 재시도 횟수 초과 시 재시도 카운터 초기화 this.retryCount = 0; this.processingQueue = false; } } } // 서버별로 로그 그룹화하는 함수 groupLogsByServer(logs) { const groups = {}; for (const log of logs) { const serverNames = log.serverNames || Object.keys(this.reportServers); for (const serverName of serverNames) { if (!groups[serverName]) groups[serverName] = []; groups[serverName].push(log); } } return groups; } // 배치 모드로 로그 전송 async sendBatchLogs(server, logs, headers) { try { // 로그 데이터 준비 const cleanLogs = logs.map(log => { // 원본 로그 데이터 보존 const originalLog = { ...log }; const formattedText = (0, utils_1.formatLogString)(originalLog, false); const payload = { text: formattedText, timestamp: log.timestamp || '', level: log.level || 'info', project: log.project || '', file: log.file || '', function: log.function || '', os: log.os || '', path: log.path || '', line: log.line || '', log: log.log }; // slackChannel 정보가 있으면 추가 if (server.slackChannel) payload.channel = server.slackChannel; return payload; }); // 로그 전송 const response = await fetch(server.url, { method: 'POST', headers, body: JSON.stringify(cleanLogs), // 압축 전송 활성화 ...(typeof Request !== 'undefined' && 'compress' in Request.prototype ? { compress: true } : {}) }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); } catch (error) { throw new Error(`Failed to send batch logs: ${error}`); } } // 단건 모드로 로그 전송 async sendSingleLog(server, log, headers) { try { // 원본 로그 데이터 보존 const originalLog = { ...log }; const formattedText = (0, utils_1.formatLogString)(originalLog, false); // 로그 데이터 준비 const payload = { text: formattedText, timestamp: log.timestamp || '', level: log.level || 'info', project: log.project || '', file: log.file || '', function: log.function || '', os: log.os || '', path: log.path || '', line: log.line || '', log: log.log }; // slackChannel 정보가 있으면 추가 if (server.slackChannel) payload.channel = server.slackChannel; // 로그 전송 const response = await fetch(server.url, { method: 'POST', headers, body: JSON.stringify(payload), // 압축 전송 ...(typeof Request !== 'undefined' && 'compress' in Request.prototype ? { compress: true } : {}) }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); } catch (error) { throw new Error(`Failed to send single log: ${error}`); } } // 재시도 스케줄링 scheduleRetry() { if (this.retryTimeoutId) clearTimeout(this.retryTimeoutId); this.retryTimeoutId = setTimeout(() => { this.retryTimeoutId = null; if (this.checkNetworkStatus()) this.processQueue(); else this.scheduleRetry(); }, this.retryInterval); } // 로그 레벨별 함수 debug(data) { return this.createLog(data, types_1.LogLevel.DEBUG); } log(data) { return this.info(data); } info(data) { return this.createLog(data, types_1.LogLevel.INFO); } warn(data) { return this.createLog(data, types_1.LogLevel.WARN); } error(data) { return this.createLog(data, types_1.LogLevel.ERROR); } fatal(data) { return this.createLog(data, types_1.LogLevel.FATAL); } // 정적 함수로 직접 호출 static log(data) { return this.getInstance().info(data); } static debug(data) { return this.getInstance().debug(data); } static info(data) { return this.getInstance().info(data); } static warn(data) { return this.getInstance().warn(data); } static error(data) { return this.getInstance().error(data); } static fatal(data) { return this.getInstance().fatal(data); } // 별도의 요청이 없을 경우에도 초기화 (패키지로인한 의도치않은 에러를 막기위함..) static ensureInitialized() { if (!this.instance) { console.warn('[Logger] Logger was not explicitly initialized. Using default configuration.'); this.initialize({ utc: 9, reportServers: {}, projectName: 'app' }); } } } exports.Logger = Logger; Logger.instance = null; var types_2 = require("./types"); Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function () { return types_2.LogLevel; } }); exports.default = Logger;