import React, { useState, useEffect } from 'react'; import { Clock, Globe, Activity, Filter, ChevronDown, ChevronUp, Calendar, BarChart3, RefreshCw } from 'lucide-react'; interface RequestLog { id: number; request_id: string; endpoint: string; method: string; path: string; query_params?: any; request_body?: any; status_code: number; response_size?: number; user_agent?: string; client_ip?: string; response_time_ms?: number; created_at?: string; } interface RequestLogStats { 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 RequestLogViewer: React.FC = () => { const [logs, setLogs] = useState([]); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [showFilters, setShowFilters] = useState(false); const [showStats, setShowStats] = useState(false); const [expandedLog, setExpandedLog] = useState(null); // Filter state const [filters, setFilters] = useState({ start_date: '', // Remove default date filter for debugging end_date: '', // Remove default date filter for debugging method: '', status_code: '', endpoint: '', page: 1, page_size: 50 }); function getDefaultDateRange() { const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - 7); // Last 7 days for requests return { start_date: startDate.toISOString().split('T')[0], end_date: endDate.toISOString().split('T')[0] }; } const fetchLogs = async () => { setLoading(true); try { const params = new URLSearchParams(); Object.entries(filters).forEach(([key, value]) => { if (value) { // Add timezone info for date parameters 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()); } } }); const response = await fetch(`/api/v1/admin/requests/logs?${params}`); if (response.ok) { const data = await response.json(); setLogs(data.items || []); } } catch (error) { console.error('Error fetching request logs:', error); } setLoading(false); }; const fetchStats = async () => { try { const params = new URLSearchParams(); if (filters.start_date) params.append('start_date', filters.start_date + 'T00:00:00Z'); if (filters.end_date) params.append('end_date', filters.end_date + 'T23:59:59Z'); const response = await fetch(`/api/v1/admin/requests/stats?${params}`); if (response.ok) { const data = await response.json(); setStats(data); } } catch (error) { console.error('Error fetching request stats:', error); } }; useEffect(() => { fetchLogs(); fetchStats(); }, [filters]); 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`; }; return (
{/* Header */}

요청 로그

API 요청 내역 및 통계를 확인할 수 있습니다

{/* Statistics */} {showStats && stats && (

총 요청

{stats.total_requests.toLocaleString()}

성공률

{stats.success_rate.toFixed(1)}%

평균 응답시간

{formatResponseTime(stats.average_response_time_ms)}

서버 에러

{stats.server_error_requests.toLocaleString()}

)} {/* 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 상세
{log.created_at ? new Date(log.created_at).toLocaleString('ko-KR') : 'N/A'} {log.method} {log.endpoint} {log.status_code} {formatResponseTime(log.response_time_ms)} {formatSize(log.response_size)} {log.client_ip || 'N/A'}

요청 정보

Request ID: {log.request_id}

Full Path: {log.path}

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

{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)}
                                      
)}
{logs.length === 0 && (

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

)}
)}
); }; export default RequestLogViewer;