Files
sam-react-prod/src/lib/api/logger.ts

361 lines
7.7 KiB
TypeScript
Raw Normal View History

// API 호출 로깅 유틸리티
// 개발 중 API 요청/응답을 추적하고 디버깅하기 위한 로거
/**
*
*/
export enum LogLevel {
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
}
/**
* API
*/
interface ApiLogEntry {
timestamp: string;
level: LogLevel;
method: string;
url: string;
requestData?: any;
responseData?: any;
statusCode?: number;
error?: Error;
duration?: number;
}
/**
* API Logger
*/
class ApiLogger {
private enabled: boolean;
private logs: ApiLogEntry[] = [];
private maxLogs: number = 100;
constructor() {
// 개발 환경에서만 로깅 활성화
this.enabled =
process.env.NODE_ENV === 'development' ||
process.env.NEXT_PUBLIC_API_LOGGING === 'true';
}
/**
*
*/
setEnabled(enabled: boolean) {
this.enabled = enabled;
}
/**
*
*/
setMaxLogs(max: number) {
this.maxLogs = max;
}
/**
* API
*/
logRequest(method: string, url: string, data?: any): number {
if (!this.enabled) return Date.now();
const startTime = Date.now();
const entry: ApiLogEntry = {
timestamp: new Date().toISOString(),
level: LogLevel.INFO,
method,
url,
requestData: data,
};
console.group(`🚀 API Request: ${method} ${url}`);
console.log('⏰ Time:', entry.timestamp);
if (data) {
console.log('📤 Request Data:', data);
}
console.groupEnd();
this.addLog(entry);
return startTime;
}
/**
* API
*/
logResponse(
method: string,
url: string,
statusCode: number,
data: any,
startTime: number
) {
if (!this.enabled) return;
const duration = Date.now() - startTime;
const entry: ApiLogEntry = {
timestamp: new Date().toISOString(),
level: LogLevel.INFO,
method,
url,
responseData: data,
statusCode,
duration,
};
console.group(`✅ API Response: ${method} ${url}`);
console.log('⏰ Time:', entry.timestamp);
console.log('📊 Status:', statusCode);
console.log('⏱️ Duration:', `${duration}ms`);
console.log('📥 Response Data:', data);
console.groupEnd();
this.addLog(entry);
}
/**
* API
*/
logError(
method: string,
url: string,
error: Error,
statusCode?: number,
startTime?: number
) {
if (!this.enabled) return;
const duration = startTime ? Date.now() - startTime : undefined;
const entry: ApiLogEntry = {
timestamp: new Date().toISOString(),
level: LogLevel.ERROR,
method,
url,
error,
statusCode,
duration,
};
console.group(`❌ API Error: ${method} ${url}`);
console.log('⏰ Time:', entry.timestamp);
if (statusCode) {
console.log('📊 Status:', statusCode);
}
if (duration) {
console.log('⏱️ Duration:', `${duration}ms`);
}
console.error('💥 Error:', error);
console.groupEnd();
this.addLog(entry);
}
/**
*
*/
logWarning(message: string, data?: any) {
if (!this.enabled) return;
const entry: ApiLogEntry = {
timestamp: new Date().toISOString(),
level: LogLevel.WARN,
method: 'WARN',
url: message,
requestData: data,
};
console.warn(`⚠️ API Warning: ${message}`, data);
this.addLog(entry);
}
/**
*
*/
logDebug(message: string, data?: any) {
if (!this.enabled) return;
const entry: ApiLogEntry = {
timestamp: new Date().toISOString(),
level: LogLevel.DEBUG,
method: 'DEBUG',
url: message,
requestData: data,
};
console.debug(`🔍 API Debug: ${message}`, data);
this.addLog(entry);
}
/**
*
*/
private addLog(entry: ApiLogEntry) {
this.logs.push(entry);
if (this.logs.length > this.maxLogs) {
this.logs.shift(); // 가장 오래된 로그 제거
}
}
/**
*
*/
getLogs(): ApiLogEntry[] {
return [...this.logs];
}
/**
*
*/
getLogsByLevel(level: LogLevel): ApiLogEntry[] {
return this.logs.filter((log) => log.level === level);
}
/**
*
*/
getErrors(): ApiLogEntry[] {
return this.getLogsByLevel(LogLevel.ERROR);
}
/**
*
*/
clearLogs() {
this.logs = [];
console.log('🗑️ API logs cleared');
}
/**
*
*/
getStats() {
const stats = {
total: this.logs.length,
byLevel: {
[LogLevel.DEBUG]: 0,
[LogLevel.INFO]: 0,
[LogLevel.WARN]: 0,
[LogLevel.ERROR]: 0,
},
averageDuration: 0,
errorRate: 0,
};
let totalDuration = 0;
let countWithDuration = 0;
this.logs.forEach((log) => {
stats.byLevel[log.level]++;
if (log.duration) {
totalDuration += log.duration;
countWithDuration++;
}
});
if (countWithDuration > 0) {
stats.averageDuration = totalDuration / countWithDuration;
}
if (stats.total > 0) {
stats.errorRate =
(stats.byLevel[LogLevel.ERROR] / stats.total) * 100;
}
return stats;
}
/**
*
*/
printStats() {
const stats = this.getStats();
console.group('📊 API Logger Statistics');
console.log('Total Logs:', stats.total);
console.log('By Level:', stats.byLevel);
console.log(
'Average Duration:',
`${stats.averageDuration.toFixed(2)}ms`
);
console.log('Error Rate:', `${stats.errorRate.toFixed(2)}%`);
console.groupEnd();
}
/**
* JSON으로
*/
exportLogs(): string {
return JSON.stringify(this.logs, null, 2);
}
/**
*
*/
printLogsAsTable() {
if (this.logs.length === 0) {
console.log('📭 No logs available');
return;
}
const tableData = this.logs.map((log) => ({
Timestamp: log.timestamp,
Level: log.level,
Method: log.method,
URL: log.url,
Status: log.statusCode || '-',
Duration: log.duration ? `${log.duration}ms` : '-',
Error: log.error?.message || '-',
}));
console.table(tableData);
}
}
// 싱글톤 인스턴스 생성
export const apiLogger = new ApiLogger();
/**
* API
* /
*/
export async function loggedFetch<T>(
method: string,
url: string,
options?: RequestInit
): Promise<T> {
const startTime = apiLogger.logRequest(method, url, options?.body);
try {
const response = await fetch(url, {
...options,
method,
});
const data = await response.json();
apiLogger.logResponse(method, url, response.status, data, startTime);
if (!response.ok) {
throw new Error(data.message || 'API request failed');
}
return data;
} catch (error) {
apiLogger.logError(method, url, error as Error, undefined, startTime);
throw error;
}
}
// 개발 도구를 window에 노출 (브라우저 콘솔에서 사용 가능)
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
(window as any).apiLogger = apiLogger;
console.log(
'💡 API Logger is available in console as "apiLogger"\n' +
' - apiLogger.getLogs() - View all logs\n' +
' - apiLogger.getErrors() - View errors only\n' +
' - apiLogger.printStats() - View statistics\n' +
' - apiLogger.printLogsAsTable() - View logs as table\n' +
' - apiLogger.clearLogs() - Clear all logs'
);
}