พัฒนา API Gateway Rate Limiting Logic และ Global Auditing Logic ในฝั่ง Backend (Django DRF)
This commit is contained in:
parent
40d0fd72f8
commit
5e9e0da972
37
backend/api/migrations/0001_initial.py
Normal file
37
backend/api/migrations/0001_initial.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-14 23:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('model_registry', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='InferenceAuditLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='เวลาเรียกใช้')),
|
||||
('endpoint_url', models.CharField(max_length=500, verbose_name='FastAPI Endpoint')),
|
||||
('http_status', models.IntegerField(verbose_name='HTTP Status Code')),
|
||||
('latency_ms', models.FloatField(verbose_name='Latency (ms)')),
|
||||
('is_success', models.BooleanField(default=False, verbose_name='สำเร็จหรือไม่')),
|
||||
('response_summary', models.TextField(blank=True, null=True, verbose_name='ผลลัพธ์สรุป/ข้อความ error')),
|
||||
('model', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='model_registry.aimodel', verbose_name='Model ที่ถูกเรียก')),
|
||||
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='ผู้ใช้งาน')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Inference Audit Log',
|
||||
'verbose_name_plural': 'Inference Audit Logs',
|
||||
'ordering': ['-timestamp'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,3 +1,26 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from model_registry.models import AiModel
|
||||
|
||||
# Create your models here.
|
||||
class InferenceAuditLog(models.Model):
|
||||
"""บันทึกทุกคำสั่งรัน Inference ที่เข้ามาใน Gateway"""
|
||||
|
||||
# ข้อมูลผู้ใช้/โมเดล
|
||||
# ใช้ ForeignKey ไปยัง CustomUser และ AiModel
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, verbose_name="ผู้ใช้งาน")
|
||||
model = models.ForeignKey(AiModel, on_delete=models.SET_NULL, null=True, verbose_name="Model ที่ถูกเรียก")
|
||||
|
||||
# ข้อมูลการประมวลผล
|
||||
timestamp = models.DateTimeField(auto_now_add=True, verbose_name="เวลาเรียกใช้")
|
||||
endpoint_url = models.CharField(max_length=500, verbose_name="FastAPI Endpoint")
|
||||
http_status = models.IntegerField(verbose_name="HTTP Status Code")
|
||||
latency_ms = models.FloatField(verbose_name="Latency (ms)")
|
||||
is_success = models.BooleanField(default=False, verbose_name="สำเร็จหรือไม่")
|
||||
|
||||
# ผลลัพธ์
|
||||
response_summary = models.TextField(blank=True, null=True, verbose_name="ผลลัพธ์สรุป/ข้อความ error")
|
||||
|
||||
class Meta:
|
||||
ordering = ['-timestamp']
|
||||
verbose_name = "Inference Audit Log"
|
||||
verbose_name_plural = "Inference Audit Logs"
|
||||
|
||||
0
backend/api/serializers/__init__.py
Normal file
0
backend/api/serializers/__init__.py
Normal file
16
backend/api/serializers/audit_serializer.py
Normal file
16
backend/api/serializers/audit_serializer.py
Normal file
@ -0,0 +1,16 @@
|
||||
from rest_framework import serializers
|
||||
from api.models import InferenceAuditLog
|
||||
|
||||
class InferenceAuditLogSerializer(serializers.ModelSerializer):
|
||||
# แสดงข้อมูล Model และ User ที่เกี่ยวข้อง
|
||||
model_name = serializers.CharField(source='model.name', read_only=True)
|
||||
username = serializers.CharField(source='user.username', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = InferenceAuditLog
|
||||
fields = (
|
||||
'id', 'user', 'username', 'model', 'model_name',
|
||||
'timestamp', 'http_status', 'latency_ms',
|
||||
'is_success', 'response_summary'
|
||||
)
|
||||
read_only_fields = fields
|
||||
65
backend/api/views/audit_viewset.py
Normal file
65
backend/api/views/audit_viewset.py
Normal file
@ -0,0 +1,65 @@
|
||||
from rest_framework import viewsets, permissions, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from django.db.models import Avg, Count, Q
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
from api.models import InferenceAuditLog
|
||||
from api.serializers.audit_serializer import InferenceAuditLogSerializer
|
||||
from permissions.permission_classes import IsAdminOrManager # ใช้สิทธิ์เดียวกันกับ Model Registry
|
||||
|
||||
class AuditLogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
API สำหรับการเข้าถึง Inference Audit Log และสถิติรวม
|
||||
"""
|
||||
queryset = InferenceAuditLog.objects.all()
|
||||
serializer_class = InferenceAuditLogSerializer
|
||||
permission_classes = [permissions.IsAuthenticated] # อนุญาตให้เข้าถึงเมื่อล็อกอินแล้ว
|
||||
|
||||
# ใช้ 'id' เป็นฟิลด์ค้นหา
|
||||
lookup_field = 'id'
|
||||
|
||||
# ใช้ 'id' เป็นชื่อพารามิเตอร์ใน URL
|
||||
lookup_url_kwarg = 'id'
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
# บังคับให้ Lookup Key (pk) เป็น String
|
||||
kwargs[self.lookup_url_kwarg] = str(kwargs[self.lookup_url_kwarg])
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
# คืน Log ล่าสุด 10 รายการ (สำหรับ Recent Events ใน Dashboard)
|
||||
return self.queryset.select_related('model', 'user')[:10]
|
||||
|
||||
# -----------------------------------------------
|
||||
# Custom Action: ดึงสถิติรวมสำหรับ Dashboard
|
||||
# Endpoint: GET /api/v1/audit/inference-summary/
|
||||
# -----------------------------------------------
|
||||
@action(detail=False, methods=['get'], url_path='inference-summary')
|
||||
def get_summary(self, request):
|
||||
one_day_ago = timezone.now() - timedelta(hours=24)
|
||||
|
||||
# 1. คำนวณสถิติรวม (Global Metrics)
|
||||
metrics = self.queryset.filter(timestamp__gte=one_day_ago).aggregate(
|
||||
total_runs=Count('id'),
|
||||
success_count=Count('id', filter=Q(is_success=True)),
|
||||
avg_latency_ms=Avg('latency_ms')
|
||||
)
|
||||
|
||||
# 2. คำนวณ Success Rate
|
||||
total = metrics.get('total_runs', 0)
|
||||
success = metrics.get('success_count', 0)
|
||||
success_rate = (success / total) * 100 if total > 0 else 0
|
||||
|
||||
# 3. คำนวณสถิติแยกตาม Model (แผนในอนาคต)
|
||||
# model_stats = self.queryset.filter(...).values('model__name').annotate(...)
|
||||
|
||||
return Response({
|
||||
"time_window": "24 hours",
|
||||
"total_runs": total,
|
||||
"success_rate": round(success_rate, 2),
|
||||
"avg_latency_ms": round(metrics.get('avg_latency_ms', 0) or 0, 2),
|
||||
"last_logs": InferenceAuditLogSerializer(self.get_queryset(), many=True).data
|
||||
})
|
||||
@ -189,12 +189,20 @@ REST_FRAMEWORK = {
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
),
|
||||
|
||||
# เพิ่มการตั้งค่าเพื่อรองรับตัวเลขขนาดใหญ่ใน JSON
|
||||
'COERCE_DECIMAL_TO_STRING': False, # ตั้งค่านี้ไว้เผื่อ
|
||||
|
||||
# ตัวเลขขนาดใหญ่ (เช่น BigIntegerField) ต้องถูก Serialize เป็น String
|
||||
'DATETIME_FORMAT': "%Y-%m-%d %H:%M:%S",
|
||||
|
||||
'DEFAULT_THROTTLE_CLASSES': [
|
||||
'rest_framework.throttling.AnonRateThrottle',
|
||||
'rest_framework.throttling.UserRateThrottle',
|
||||
],
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'anon': '100/day',
|
||||
'user': '50/minute',
|
||||
'inference_burst': '20/minute', # Rate Limit สำหรับงานหนัก
|
||||
}
|
||||
}
|
||||
|
||||
# 3. ตั้งค่า DJOSER (เพื่อจัดการ Auth Endpoints)
|
||||
|
||||
@ -21,8 +21,9 @@ from rest_framework.routers import DefaultRouter
|
||||
from model_registry.views.ai_model_viewset import AiModelRegistryViewSet
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
|
||||
|
||||
# Import Health Check View ในแอพ /api
|
||||
# Import View ในแอพ /api
|
||||
from api.views.health_check_view import SystemHealthCheck
|
||||
from api.views.audit_viewset import AuditLogViewSet
|
||||
|
||||
# 1. กำหนดตัวแปร router ก่อนใช้งาน
|
||||
router = DefaultRouter()
|
||||
@ -32,7 +33,13 @@ router = DefaultRouter()
|
||||
router.register(
|
||||
r'models',
|
||||
AiModelRegistryViewSet,
|
||||
basename='aimodel' # basename จำเป็นเมื่อ ViewSet ไม่ได้สืบทอดจาก Model
|
||||
basename='aimodel'
|
||||
)
|
||||
# URL: /api/v1/audit/ (AuditLogViewSet)
|
||||
router.register(
|
||||
r'audit',
|
||||
AuditLogViewSet,
|
||||
basename='auditlog',
|
||||
)
|
||||
|
||||
# 3. ลงทะเบียน ViewSet อื่น ๆ
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from ..models import AiModel
|
||||
from typing import List, Optional
|
||||
from api.models import InferenceAuditLog
|
||||
|
||||
class AiModelRepository:
|
||||
"""จัดการการเข้าถึงฐานข้อมูลสำหรับ AiModel โดยเฉพาะ"""
|
||||
@ -20,4 +21,27 @@ class AiModelRepository:
|
||||
|
||||
def delete(self, model_instance: AiModel):
|
||||
model_instance.delete()
|
||||
|
||||
def log_inference(self, model_pk: str, user_pk: int, endpoint: str,
|
||||
status_code: int, latency: float, success: bool, summary: str):
|
||||
"""บันทึก Inference Log ลงใน InferenceAuditLog Model"""
|
||||
|
||||
try:
|
||||
# 1. ดึง Instance Model และ User มาใช้ (ถ้ามี)
|
||||
model_instance = self.get_by_id(model_pk)
|
||||
|
||||
# 2. สร้าง Log Record
|
||||
InferenceAuditLog.objects.create(
|
||||
user_id=user_pk,
|
||||
model=model_instance,
|
||||
endpoint_url=endpoint,
|
||||
http_status=status_code,
|
||||
latency_ms=latency,
|
||||
is_success=success,
|
||||
response_summary=summary
|
||||
)
|
||||
except Exception as e:
|
||||
# ควรมี Loggin.error ที่นี่ เพื่อบันทึกว่าการบันทึก Log ล้มเหลว
|
||||
print(f"CRITICAL: Failed to save audit log: {e}")
|
||||
|
||||
# Logic อื่น ๆ ที่เกี่ยวข้องกับการเข้าถึง DB
|
||||
@ -5,6 +5,7 @@ import requests
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
import time
|
||||
|
||||
class ConnectionError(Exception):
|
||||
"""Custom Exception สำหรับการเชื่อมต่อล้มเหลว"""
|
||||
@ -88,6 +89,8 @@ class AiModelService:
|
||||
|
||||
# --- Proxy Execution Logic ---
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# 1. สร้าง Full Inference URL
|
||||
full_url = model.full_inference_url()
|
||||
|
||||
@ -122,11 +125,36 @@ class AiModelService:
|
||||
cache.set(cache_key, result, timeout=3600)
|
||||
|
||||
# 7. บันทึก Audit Log (Logic ในอนาคต)
|
||||
# self.repo.log_inference_request(pk, user_id, full_url, response.status_code)
|
||||
end_time = time.time()
|
||||
latency = round((end_time - start_time) * 1000, 2)
|
||||
summary = "Inference completed successfully."
|
||||
self.repo.log_inference(
|
||||
model_pk=str(pk), # ส่งเป็น String
|
||||
user_pk=user_id,
|
||||
endpoint=full_url,
|
||||
status_code=response.status_code,
|
||||
latency=latency,
|
||||
success=True,
|
||||
summary=summary
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
end_time = time.time()
|
||||
latency = round((end_time - start_time) * 1000, 2)
|
||||
summary = f"HTTP Error {e.response.status_code}: {e.response.text[:200]}"
|
||||
|
||||
# บันทึก Audit Log เมื่อล้มเหลว (HTTP Error)
|
||||
self.repo.log_inference(
|
||||
model_pk=str(pk),
|
||||
user_pk=user_id,
|
||||
endpoint=full_url,
|
||||
status_code=e.response.status_code,
|
||||
latency=latency,
|
||||
success=False,
|
||||
summary=summary
|
||||
)
|
||||
detail = f"AI Service returned error {response.status_code}: {response.text}"
|
||||
raise ConnectionError(detail)
|
||||
except requests.exceptions.RequestException as e:
|
||||
|
||||
@ -12,6 +12,8 @@ from permissions.permission_classes import IsAdminOrManager
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
|
||||
# Dependency Injection: สร้าง Instance ของ Repository และ Service
|
||||
# สำหรับโปรเจกต์ขนาดเล็กสามารถทำแบบนี้ได้
|
||||
repo = AiModelRepository()
|
||||
@ -117,7 +119,8 @@ class AiModelRegistryViewSet(viewsets.ModelViewSet):
|
||||
methods=['post'],
|
||||
url_path='run-inference',
|
||||
parser_classes=[MultiPartParser],
|
||||
permission_classes=[IsAuthenticated]
|
||||
permission_classes=[IsAuthenticated],
|
||||
throttle_classes=[UserRateThrottle] # ถ้าผู้ใช้เรียกเกิน 50 ครั้ง/นาที จะถูกปฏิเสธด้วย 429 Too Many Requests
|
||||
)
|
||||
def run_inference(self, request, pk=None):
|
||||
"""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user