import React, { useState, useEffect } from 'react'; import { adminApi } from '@/lib/api'; import { Clock, Globe, Activity, Filter, ChevronDown, ChevronUp, Calendar, BarChart3, RefreshCw, AlertTriangle, CheckCircle, XCircle, AlertCircle, Trash2 } from 'lucide-react'; interface LogEntry { id: number; request_id: string; endpoint: string; method: string; path: string; query_params?: any; request_body?: any; headers?: any; status_code: number; response_size?: number; user_agent?: string; client_ip?: string; response_time_ms?: number; created_at?: string; // Error specific fields error_type?: string; error_message?: string; error_detail?: any; stack_trace?: string; is_error?: boolean; } interface LogStats { total_requests: number; success_requests: number; client_error_requests: number; server_error_requests: number; success_rate: number; requests_by_method: { [key: string]: number }; requests_by_status_code: { [key: string]: number }; requests_by_endpoint: { [key: string]: number }; average_response_time_ms: number; start_date: string; end_date: string; } const UnifiedLogViewer: React.FC = () => { const [isClient, setIsClient] = useState(false); const [logs, setLogs] = useState([]); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [showFilters, setShowFilters] = useState(false); const [showStats, setShowStats] = useState(true); const [expandedLog, setExpandedLog] = useState(null); const [logType, setLogType] = useState<'all' | 'errors' | 'success' | 'data'>('data'); // Filter state const [filters, setFilters] = useState({ start_date: '', end_date: '', method: '', status_code: '', endpoint: '', page: 1, page_size: 50 }); const fetchLogs = async () => { setLoading(true); try { const params = new URLSearchParams(); Object.entries(filters).forEach(([key, value]) => { if (value) { if (key === 'start_date') { params.append(key, value.toString() + 'T00:00:00Z'); } else if (key === 'end_date') { params.append(key, value.toString() + 'T23:59:59Z'); } else { params.append(key, value.toString()); } } }); // Filter by log type if (logType === 'errors') { params.append('min_status_code', '500'); } else if (logType === 'success') { params.append('status_code', '200'); } let allLogs: LogEntry[] = []; if (logType === 'data') { // Fetch financial and price data requests separately and combine try { const financialParams = new URLSearchParams(params); financialParams.append('endpoint', '/api/v1/financial/*'); const priceParams = new URLSearchParams(params); priceParams.append('endpoint', '/api/v1/price/*'); const [financialData, priceData] = await Promise.all([ adminApi.getRequestLogs(Object.fromEntries(financialParams)), adminApi.getRequestLogs(Object.fromEntries(priceParams)) ]); // Combine and sort by created_at allLogs = [...(financialData.items || []), ...(priceData.items || [])].sort((a: LogEntry, b: LogEntry) => new Date(b.created_at || '').getTime() - new Date(a.created_at || '').getTime() ); } catch (error) { console.error('Error fetching data logs:', error); } } else { try { const data = await adminApi.getRequestLogs(Object.fromEntries(params)); allLogs = data.items || []; } catch (error) { console.error('Error fetching logs:', error); } } // Fetch error details for 500 errors const enrichedLogs = await Promise.all( allLogs.map(async (log: LogEntry) => { if (log.status_code >= 500) { try { const errorData = await adminApi.getErrorByRequestId(log.request_id); return { ...log, is_error: true, error_type: errorData.error_type, error_message: errorData.error_message, error_detail: errorData.error_detail, stack_trace: errorData.stack_trace }; } catch (error) { console.error('Error fetching error details:', error); } } return { ...log, is_error: log.status_code >= 400 }; }) ); setLogs(enrichedLogs); } catch (error) { console.error('Error fetching logs:', error); } setLoading(false); }; const fetchStats = async () => { try { const params: Record = {}; if (filters.start_date) params.start_date = filters.start_date + 'T00:00:00Z'; if (filters.end_date) params.end_date = filters.end_date + 'T23:59:59Z'; const data = await adminApi.getRequestStats(params); setStats(data); } catch (error) { console.error('Error fetching stats:', error); } }; const clearAllLogs = async () => { if (!window.confirm('정말로 모든 로그를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.')) { return; } try { setLoading(true); // Clear both request logs and error logs const [requestResult, errorResult] = await Promise.all([ adminApi.deleteRequestLogs({ confirm: 'true' }), adminApi.deleteErrorLogs({ confirm: 'true' }) ]); alert(`성공적으로 삭제되었습니다:\n- 요청 로그: ${requestResult.deleted_count}개\n- 에러 로그: ${errorResult.deleted_count}개`); // Refresh data await fetchLogs(); await fetchStats(); } catch (error) { console.error('Error clearing logs:', error); alert('로그 삭제 중 오류가 발생했습니다.'); } finally { setLoading(false); } }; useEffect(() => { setIsClient(true); }, []); useEffect(() => { if (isClient) { fetchLogs(); fetchStats(); } }, [filters, logType, isClient]); const getStatusIcon = (statusCode: number) => { if (statusCode >= 200 && statusCode < 300) return ; if (statusCode >= 400 && statusCode < 500) return ; if (statusCode >= 500) return ; return ; }; const getStatusCodeColor = (statusCode: number): string => { if (statusCode >= 200 && statusCode < 300) return 'text-green-600 bg-green-50'; if (statusCode >= 300 && statusCode < 400) return 'text-blue-600 bg-blue-50'; if (statusCode >= 400 && statusCode < 500) return 'text-yellow-600 bg-yellow-50'; if (statusCode >= 500) return 'text-red-600 bg-red-50'; return 'text-gray-600 bg-gray-50'; }; const getMethodColor = (method: string): string => { const colors: Record = { 'GET': 'text-green-600 bg-green-50', 'POST': 'text-blue-600 bg-blue-50', 'PUT': 'text-yellow-600 bg-yellow-50', 'DELETE': 'text-red-600 bg-red-50', 'PATCH': 'text-purple-600 bg-purple-50' }; return colors[method] || 'text-gray-600 bg-gray-50'; }; const formatResponseTime = (ms?: number): string => { if (!ms) return 'N/A'; if (ms < 1000) return `${ms.toFixed(0)}ms`; return `${(ms / 1000).toFixed(2)}s`; }; const formatSize = (bytes?: number): string => { if (!bytes) return 'N/A'; if (bytes < 1024) return `${bytes}B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`; return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; }; const formatDateTime = (dateString?: string): string => { if (!dateString || !isClient) return 'N/A'; try { return new Date(dateString).toLocaleString('ko-KR'); } catch (error) { return dateString; } }; if (!isClient) { return (

로그 시스템 초기화 중...

); } return (
{/* Header */}

시스템 로그

모든 API 요청 및 에러 로그를 확인할 수 있습니다

{/* Log Type Selector */}
{/* Statistics */} {showStats && (

총 요청

{stats?.total_requests !== undefined ? stats.total_requests.toLocaleString() : 'Loading...'}

성공

{stats?.success_requests !== undefined ? stats.success_requests.toLocaleString() : 'Loading...'}

클라이언트 에러

{stats?.client_error_requests !== undefined ? stats.client_error_requests.toLocaleString() : 'Loading...'}

서버 에러

{stats?.server_error_requests !== undefined ? stats.server_error_requests.toLocaleString() : 'Loading...'}

평균 응답시간

{stats?.average_response_time_ms !== undefined ? formatResponseTime(stats.average_response_time_ms) : 'Loading...'}

)} {/* Filters */} {showFilters && (
setFilters({ ...filters, start_date: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" />
setFilters({ ...filters, end_date: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" />
setFilters({ ...filters, status_code: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" />
setFilters({ ...filters, endpoint: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" />
)} {/* Logs Table */}
{loading ? (

로그를 불러오는 중...

) : (
{logs.map((log) => ( {expandedLog === log.id && ( )} ))}
상태 시간 메서드 엔드포인트 상태코드 응답시간 IP 상세
{getStatusIcon(log.status_code)} {formatDateTime(log.created_at)} {log.method} {log.endpoint} {log.status_code} {formatResponseTime(log.response_time_ms)} {log.client_ip || 'N/A'}
{/* Request Information */}

요청 정보

Request ID: {log.request_id}

Full Path: {log.path}

User Agent: {log.user_agent || 'N/A'}

{/* Data Source Info for successful requests */} {log.status_code >= 200 && log.status_code < 300 && log.headers?.['X-Data-Source'] && (

데이터 소스: {log.headers['X-Data-Source'] === 'database-cache' ? '🗄️ DB 캐시' : log.headers['X-Data-Source'] === 'yfinance-fresh' ? '🔄 Yahoo Finance (신규)' : log.headers['X-Data-Source'] === 'yfinance-partial' ? '📊 Yahoo Finance (부분)' : log.headers['X-Data-Source']}

)} {log.headers && Object.keys(log.headers).length > 0 && (
Headers:
                                        {JSON.stringify(log.headers, null, 2)}
                                      
)} {log.query_params && Object.keys(log.query_params).length > 0 && (
Query Parameters:
                                        {JSON.stringify(log.query_params, null, 2)}
                                      
)} {log.request_body && (
Request Body:
                                        {JSON.stringify(log.request_body, null, 2)}
                                      
)}
{/* Error Information */} {log.is_error && (log.error_message || log.status_code >= 400) && (

에러 정보

Status Code: {log.status_code}

{log.error_type && (

Error Type: {log.error_type}

)} {log.error_message && (

Error Message: {log.error_message}

)} {/* Error Response Body - show raw response from error_detail */} {log.error_detail && (
에러 응답:
                                          {typeof log.error_detail === 'string' ? log.error_detail : JSON.stringify(log.error_detail, null, 2)}
                                        
)} {log.stack_trace && (
Stack Trace:
                                          {log.stack_trace}
                                        
)}
)}
{logs.length === 0 && (

조건에 맞는 로그가 없습니다.

)}
)}
); }; export default UnifiedLogViewer;