from django.db.models import Count, Avg, Max, Q, F from django.db.models.functions import TruncHour, TruncDate from django.utils import timezone from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated, IsAdminUser from rest_framework.response import Response from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter, OrderingFilter from .models import ApiRequestLog, ErrorLog, SystemEventLog from .serializers import ( ApiRequestLogSerializer, ApiRequestLogListSerializer, ErrorLogSerializer, ErrorLogListSerializer, ErrorLogResolveSerializer, SystemEventLogSerializer, SystemEventLogListSerializer, ) from .filters import ApiRequestLogFilter, ErrorLogFilter, SystemEventLogFilter class ApiRequestLogViewSet(viewsets.ReadOnlyModelViewSet): queryset = ApiRequestLog.objects.all() permission_classes = [IsAuthenticated, IsAdminUser] filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_class = ApiRequestLogFilter search_fields = ['path', 'username', 'ip_address'] ordering_fields = ['timestamp', 'duration_ms', 'status_code'] ordering = ['-timestamp'] def get_serializer_class(self): if self.action == 'list': return ApiRequestLogListSerializer return ApiRequestLogSerializer class ErrorLogViewSet(viewsets.ModelViewSet): queryset = ErrorLog.objects.all() permission_classes = [IsAuthenticated, IsAdminUser] filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_class = ErrorLogFilter search_fields = ['message', 'module', 'exception_type', 'path'] ordering_fields = ['timestamp', 'occurrence_count'] ordering = ['-timestamp'] def get_serializer_class(self): if self.action == 'list': return ErrorLogListSerializer if self.action == 'resolve': return ErrorLogResolveSerializer return ErrorLogSerializer def get_queryset(self): if self.action in ('update', 'partial_update', 'destroy'): return ErrorLog.objects.all() return ErrorLog.objects.all() @action(detail=True, methods=['post']) def resolve(self, request, pk=None): error_log = self.get_object() error_log.is_resolved = True error_log.resolved_at = timezone.now() error_log.save(update_fields=['is_resolved', 'resolved_at']) return Response({'status': 'resolved'}) @action(detail=False, methods=['post']) def resolve_all(self, request): ids = request.data.get('ids', []) if ids: ErrorLog.objects.filter(id__in=ids, is_resolved=False).update( is_resolved=True, resolved_at=timezone.now() ) return Response({'status': 'resolved', 'count': len(ids)}) return Response({'error': '请提供 ids 列表'}, status=status.HTTP_400_BAD_REQUEST) class SystemEventLogViewSet(viewsets.ReadOnlyModelViewSet): queryset = SystemEventLog.objects.select_related('user', 'target_user').all() permission_classes = [IsAuthenticated, IsAdminUser] filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_class = SystemEventLogFilter search_fields = ['description', 'user__username'] ordering_fields = ['timestamp'] ordering = ['-timestamp'] def get_serializer_class(self): if self.action == 'list': return SystemEventLogListSerializer return SystemEventLogSerializer class LogStatsViewSet(viewsets.ViewSet): permission_classes = [IsAuthenticated, IsAdminUser] @action(detail=False, methods=['get']) def summary(self, request): days = int(request.query_params.get('days', 7)) start_date = timezone.now() - timezone.timedelta(days=days) request_qs = ApiRequestLog.objects.filter(timestamp__gte=start_date) error_qs = ErrorLog.objects.filter(timestamp__gte=start_date) total_requests = request_qs.count() total_errors = request_qs.filter(is_error=True).count() error_rate = (total_errors / total_requests * 100) if total_requests > 0 else 0 duration_stats = request_qs.aggregate( avg_duration=Avg('duration_ms'), max_duration=Max('duration_ms') ) unique_users = request_qs.exclude(user__isnull=True).values('user').distinct().count() unique_ips = request_qs.exclude(ip_address__isnull=True).values('ip_address').distinct().count() top_status_codes = ( request_qs.values('status_code') .annotate(count=Count('id')) .order_by('-count')[:10] ) today_start = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) error_count_today = error_qs.filter(timestamp__gte=today_start).count() request_count_today = request_qs.filter(timestamp__gte=today_start).count() return Response({ 'total_requests': total_requests, 'total_errors': total_errors, 'error_rate': round(error_rate, 2), 'avg_duration_ms': round(duration_stats['avg_duration'] or 0, 2), 'max_duration_ms': round(duration_stats['max_duration'] or 0, 2), 'unique_users': unique_users, 'unique_ips': unique_ips, 'top_status_codes': list(top_status_codes), 'error_count_today': error_count_today, 'request_count_today': request_count_today, }) @action(detail=False, methods=['get']) def by_path(self, request): days = int(request.query_params.get('days', 7)) limit = int(request.query_params.get('limit', 20)) start_date = timezone.now() - timezone.timedelta(days=days) stats = ( ApiRequestLog.objects .filter(timestamp__gte=start_date) .values('path') .annotate( request_count=Count('id'), error_count=Count('id', filter=Q(is_error=True)), avg_duration_ms=Avg('duration_ms'), ) .order_by('-request_count')[:limit] ) result = [] for item in stats: item['error_rate'] = round( (item['error_count'] / item['request_count'] * 100) if item['request_count'] > 0 else 0, 2 ) item['avg_duration_ms'] = round(item['avg_duration_ms'] or 0, 2) result.append(item) return Response(result) @action(detail=False, methods=['get']) def by_hour(self, request): days = int(request.query_params.get('days', 1)) start_date = timezone.now() - timezone.timedelta(days=days) stats = ( ApiRequestLog.objects .filter(timestamp__gte=start_date) .annotate(hour=TruncHour('timestamp')) .values('hour') .annotate( request_count=Count('id'), error_count=Count('id', filter=Q(is_error=True)), avg_duration_ms=Avg('duration_ms'), ) .order_by('hour') ) result = [] for item in stats: item['avg_duration_ms'] = round(item['avg_duration_ms'] or 0, 2) result.append(item) return Response(result) @action(detail=False, methods=['get']) def slow_requests(self, request): days = int(request.query_params.get('days', 7)) threshold = float(request.query_params.get('threshold', 1000)) limit = int(request.query_params.get('limit', 50)) start_date = timezone.now() - timezone.timedelta(days=days) slow_requests = ( ApiRequestLog.objects .filter(timestamp__gte=start_date, duration_ms__gte=threshold) .values('path', 'method') .annotate( count=Count('id'), avg_duration_ms=Avg('duration_ms'), max_duration_ms=Max('duration_ms'), ) .order_by('-avg_duration_ms')[:limit] ) result = [] for item in slow_requests: item['avg_duration_ms'] = round(item['avg_duration_ms'] or 0, 2) item['max_duration_ms'] = round(item['max_duration_ms'] or 0, 2) result.append(item) return Response(result) @action(detail=False, methods=['get']) def error_types(self, request): days = int(request.query_params.get('days', 7)) limit = int(request.query_params.get('limit', 20)) start_date = timezone.now() - timezone.timedelta(days=days) error_types = ( ErrorLog.objects .filter(timestamp__gte=start_date) .values('exception_type', 'module') .annotate( count=Count('id'), last_occurred=Max('timestamp'), ) .order_by('-count')[:limit] ) return Response(list(error_types))