พัฒนา API Gateway Rate Limiting Logic และ Global Auditing Logic ในฝั่ง Backend (Django DRF)

This commit is contained in:
Flook 2025-11-15 06:40:19 +07:00
parent 40d0fd72f8
commit 5e9e0da972
10 changed files with 218 additions and 7 deletions

View 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'],
},
),
]

View File

@ -1,3 +1,26 @@
from django.db import models 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"

View File

View 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

View 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
})

View File

@ -189,12 +189,20 @@ REST_FRAMEWORK = {
'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer', 'rest_framework.renderers.BrowsableAPIRenderer',
), ),
# เพิ่มการตั้งค่าเพื่อรองรับตัวเลขขนาดใหญ่ใน JSON # เพิ่มการตั้งค่าเพื่อรองรับตัวเลขขนาดใหญ่ใน JSON
'COERCE_DECIMAL_TO_STRING': False, # ตั้งค่านี้ไว้เผื่อ 'COERCE_DECIMAL_TO_STRING': False, # ตั้งค่านี้ไว้เผื่อ
# ตัวเลขขนาดใหญ่ (เช่น BigIntegerField) ต้องถูก Serialize เป็น String # ตัวเลขขนาดใหญ่ (เช่น BigIntegerField) ต้องถูก Serialize เป็น String
'DATETIME_FORMAT': "%Y-%m-%d %H:%M:%S", '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) # 3. ตั้งค่า DJOSER (เพื่อจัดการ Auth Endpoints)

View File

@ -21,8 +21,9 @@ from rest_framework.routers import DefaultRouter
from model_registry.views.ai_model_viewset import AiModelRegistryViewSet from model_registry.views.ai_model_viewset import AiModelRegistryViewSet
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView 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.health_check_view import SystemHealthCheck
from api.views.audit_viewset import AuditLogViewSet
# 1. กำหนดตัวแปร router ก่อนใช้งาน # 1. กำหนดตัวแปร router ก่อนใช้งาน
router = DefaultRouter() router = DefaultRouter()
@ -32,7 +33,13 @@ router = DefaultRouter()
router.register( router.register(
r'models', r'models',
AiModelRegistryViewSet, AiModelRegistryViewSet,
basename='aimodel' # basename จำเป็นเมื่อ ViewSet ไม่ได้สืบทอดจาก Model basename='aimodel'
)
# URL: /api/v1/audit/ (AuditLogViewSet)
router.register(
r'audit',
AuditLogViewSet,
basename='auditlog',
) )
# 3. ลงทะเบียน ViewSet อื่น ๆ # 3. ลงทะเบียน ViewSet อื่น ๆ

View File

@ -1,5 +1,6 @@
from ..models import AiModel from ..models import AiModel
from typing import List, Optional from typing import List, Optional
from api.models import InferenceAuditLog
class AiModelRepository: class AiModelRepository:
"""จัดการการเข้าถึงฐานข้อมูลสำหรับ AiModel โดยเฉพาะ""" """จัดการการเข้าถึงฐานข้อมูลสำหรับ AiModel โดยเฉพาะ"""
@ -20,4 +21,27 @@ class AiModelRepository:
def delete(self, model_instance: AiModel): def delete(self, model_instance: AiModel):
model_instance.delete() 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 # Logic อื่น ๆ ที่เกี่ยวข้องกับการเข้าถึง DB

View File

@ -5,6 +5,7 @@ import requests
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.core.files.uploadedfile import UploadedFile from django.core.files.uploadedfile import UploadedFile
import time
class ConnectionError(Exception): class ConnectionError(Exception):
"""Custom Exception สำหรับการเชื่อมต่อล้มเหลว""" """Custom Exception สำหรับการเชื่อมต่อล้มเหลว"""
@ -88,6 +89,8 @@ class AiModelService:
# --- Proxy Execution Logic --- # --- Proxy Execution Logic ---
start_time = time.time()
# 1. สร้าง Full Inference URL # 1. สร้าง Full Inference URL
full_url = model.full_inference_url() full_url = model.full_inference_url()
@ -122,11 +125,36 @@ class AiModelService:
cache.set(cache_key, result, timeout=3600) cache.set(cache_key, result, timeout=3600)
# 7. บันทึก Audit Log (Logic ในอนาคต) # 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 return result
except requests.exceptions.HTTPError as e: 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}" detail = f"AI Service returned error {response.status_code}: {response.text}"
raise ConnectionError(detail) raise ConnectionError(detail)
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:

View File

@ -12,6 +12,8 @@ from permissions.permission_classes import IsAdminOrManager
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.throttling import UserRateThrottle
# Dependency Injection: สร้าง Instance ของ Repository และ Service # Dependency Injection: สร้าง Instance ของ Repository และ Service
# สำหรับโปรเจกต์ขนาดเล็กสามารถทำแบบนี้ได้ # สำหรับโปรเจกต์ขนาดเล็กสามารถทำแบบนี้ได้
repo = AiModelRepository() repo = AiModelRepository()
@ -117,7 +119,8 @@ class AiModelRegistryViewSet(viewsets.ModelViewSet):
methods=['post'], methods=['post'],
url_path='run-inference', url_path='run-inference',
parser_classes=[MultiPartParser], parser_classes=[MultiPartParser],
permission_classes=[IsAuthenticated] permission_classes=[IsAuthenticated],
throttle_classes=[UserRateThrottle] # ถ้าผู้ใช้เรียกเกิน 50 ครั้ง/นาที จะถูกปฏิเสธด้วย 429 Too Many Requests
) )
def run_inference(self, request, pk=None): def run_inference(self, request, pk=None):
""" """