@homecheck/logger
Version:
A simple logger for Web, Node, Capacitor apps.
254 lines (253 loc) • 10.1 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.getFormattedTimestamp = getFormattedTimestamp;
exports.getStackTrace = getStackTrace;
exports.getOsInfo = getOsInfo;
exports.formatLogString = formatLogString;
exports.formatDataToString = formatDataToString;
function getFormattedTimestamp(utcOffset) {
try {
const now = new Date();
// UTC 오프셋 적용
const offsetMillis = utcOffset * 60 * 60 * 1000;
const localTime = new Date(now.getTime() + offsetMillis);
const isoString = localTime.toISOString();
const [datePart, timePart] = isoString.split('T');
const timeWithoutMillis = timePart.split('.')[0];
return `${datePart} ${timeWithoutMillis}`;
}
catch (error) {
// 에러 발생 시 기본 ISO 문자열 반환
return new Date().toISOString();
}
}
function getStackTrace(project, skipFrames = 3) {
try {
const stack = new Error().stack || '';
const stackLines = stack.split('\n');
// 브라우저 환경인지 확인
const isBrowser = typeof window !== 'undefined';
// 스택 트레이스에서 Logger 내부 함수를 건너뛰기
let callerLine = '';
// Logger 클래스의 메서드를 호출한 실제 위치 찾기
for (let i = skipFrames; i < stackLines.length; i++) {
const line = stackLines[i] || '';
// Logger 내부 함수면 건너뜀
if (line.includes('@homecheck/logger') && (line.includes('/index.js') || line.includes('/utils.js')))
continue;
// 브라우저 환경에서 Logger 내부 함수 건너뛰기
if (isBrowser && (line.includes('Logger.') || line.includes('logger.') || line.includes('/logger/')))
continue;
// 외부 호출자 찾음
callerLine = line;
break;
}
// 호출자를 찾지 못한 경우 기본값
if (!callerLine && stackLines.length > skipFrames)
callerLine = stackLines[skipFrames];
// 브라우저 환경에서의 스택 트레이스 파싱
if (isBrowser) {
// 공백이 없는 형태의 스택 트레이스 처리
if (callerLine.includes('@')) {
const parts = callerLine.split('@');
const functionName = parts[0];
// URL과 라인 정보 추출
const urlParts = parts[1].split(':');
const lineNumber = urlParts[urlParts.length - 2] || '';
const columnNumber = urlParts[urlParts.length - 1] || '';
// URL 부분 재구성 (마지막 두 부분(라인, 컬럼)을 제외)
const urlBase = urlParts.slice(0, urlParts.length - 2).join(':');
// 파일명 추출
const filePath = urlBase || 'unknown';
const fileName = filePath.split('/').pop() || filePath;
return {
function: functionName,
path: filePath,
file: fileName,
project,
line: lineNumber && columnNumber ? `:${lineNumber}:${columnNumber}` : ''
};
}
let match = callerLine.match(/at\s+([^@]+)@([^:]+):(\d+):(\d+)/);
if (match) {
return {
function: match[1].trim(),
path: match[2],
file: match[2].split('/').pop() || match[2],
project,
line: `:${match[3]}:${match[4]}`
};
}
match = callerLine.match(/([^@]+)@([^:]+:[^:]+):(\d+):(\d+)/);
if (match) {
return {
function: match[1].trim(),
path: match[2],
file: match[2].split('/').pop() || match[2],
project,
line: `:${match[3]}:${match[4]}`
};
}
match = callerLine.match(/at\s+(.+?)(?:\s+\((.+?):(\d+):(\d+)\))?$/);
if (match) {
const functionName = match[1] && !match[1].includes(':') ? match[1].trim() : 'anonymous';
const filePath = match[2] || (match[1] && match[1].includes(':') ? match[1].split(':')[0] : 'unknown');
const lineNumber = match[3] || (match[1] && match[1].includes(':') ? match[1].split(':')[1] : '');
const columnNumber = match[4] || (match[1] && match[1].includes(':') ? match[1].split(':')[2] : '');
return {
function: functionName,
path: filePath,
file: filePath.split('/').pop() || filePath,
project,
line: lineNumber && columnNumber ? `:${lineNumber}:${columnNumber}` : ''
};
}
}
// Node.js 환경 또는 기본 파싱 로직
// 파일 정보
const fileMatch = callerLine.match(/\((.+)\)/) || callerLine.match(/at\s+(.+)$/);
let path = fileMatch ? fileMatch[1].split(':')[0] : 'unknown';
// 파일명
const file = path.split('/').pop() || path;
// 라인 정보
let line = '';
const lineMatch = callerLine.match(/:(\d+):(\d+)/);
if (lineMatch)
line = `:${lineMatch[1]}:${lineMatch[2]}`;
// 함수명
const functionMatch = callerLine.match(/at\s+([^(]+)\s*\(/) || callerLine.match(/at\s+(.+)$/);
let functionName = functionMatch ? functionMatch[1].trim() : 'unknown';
return {
file,
function: functionName,
project,
path,
line
};
}
catch (error) {
// 오류 발생 시 기본값
return {
file: 'unknown',
function: 'unknown',
project,
path: 'unknown',
line: ''
};
}
}
function getOsInfo() {
try {
if (typeof window !== 'undefined') {
// 브라우저 환경
const userAgent = navigator.userAgent;
// userAgent에서 플랫폼 정보 추출
let platform = 'unknown';
if (userAgent.includes('Windows'))
platform = 'Windows';
else if (userAgent.includes('Macintosh'))
platform = 'Mac';
else if (userAgent.includes('Linux'))
platform = 'Linux';
else if (userAgent.includes('Android'))
platform = 'Android';
else if (userAgent.includes('iOS') || /iPhone|iPad|iPod/.test(userAgent))
platform = 'iOS';
return `${platform} ${userAgent}`;
}
else {
// Node.js 환경
// @ts-ignore
const os = require('os');
return `${os.platform()} ${os.release()}`;
}
}
catch (error) {
return 'unknown';
}
}
function formatLogString(logData, withColors = false) {
const level = logData.level || 'info';
const timestamp = logData.timestamp || '';
const text = logData.text || '';
const functionName = logData.function || '';
const path = logData.path || '';
const line = logData.line || '';
const project = logData.project || '';
const log = logData.log || '';
const [dir, file] = path.match(/(.*\/|)([^/]*)$/).slice(1);
// 색상 코드 (withColors가 true일 때만 적용)
const levelColor = withColors ? getLevelColor(level) : '';
const resetColor = withColors ? '\x1b[0m' : '';
const dimColor = withColors ? '\x1b[2m' : '';
// 마크다운 문자 (withColors false)
const codeText = !withColors ? '`' : '';
const codeBlock = !withColors ? '```' : '';
let logString = `
${codeText}${levelColor}[${level.toUpperCase()}]${resetColor}${codeText} ${codeText}${project}${codeText} ${codeText}${dimColor}${timestamp}${resetColor}${codeText}
${codeBlock}
${dimColor}at${resetColor} ${functionName} ${dimColor}${dir}${resetColor}${file}${line}
`;
let content = '';
// 텍스트 데이터 추가
if (text && text.trim())
content += text;
if (log && log.trim()) {
if (content)
content += '\n\n';
content += log;
}
if (content)
logString += `
${content}
`;
logString += `
${codeBlock}
`;
return logString;
}
// 로그 레벨에 따른 색상 코드 반환
function getLevelColor(level) {
switch (level) {
case 'debug': return '\x1b[36m'; // 청록색
case 'info': return '\x1b[32m'; // 녹색
case 'warn': return '\x1b[33m'; // 노란색
case 'error': return '\x1b[31m'; // 빨간색
case 'fatal': return '\x1b[35m'; // 자주색
default: return '\x1b[32m'; // 기본 녹색
}
}
// 데이터를 문자열로 변환하는 함수
function formatDataToString(data) {
if (data === undefined)
return 'undefined';
if (data === null)
return 'null';
if (typeof data === 'string')
return data;
if (typeof data === 'number' || typeof data === 'boolean' || typeof data === 'bigint')
return String(data);
if (data instanceof Error) {
// 에러 객체를 문자열로 변환
let result = `${data.name || 'Error'}: ${data.message || ''}`;
// 추가 속성이 있는지 확인
const additionalProps = {};
for (const key in data) {
if (key !== 'name' && key !== 'message' && key !== 'stack')
additionalProps[key] = data[key];
}
// 추가 속성이 있으면 JSON으로 표시
if (Object.keys(additionalProps).length > 0)
result += '\n' + JSON.stringify(additionalProps, null, 2);
return result;
}
if (typeof data === 'object') {
try {
return JSON.stringify(data, null, 2);
}
catch (e) {
return '[Object]';
}
}
return String(data);
}
;