@homecheck/logger
Version:
A simple logger for Web, Node, Capacitor apps.
370 lines (369 loc) • 14.1 kB
JavaScript
"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;