Skip to content

Commit

Permalink
feat:api_test history (#759)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lawrence-lkq authored Jul 29, 2024
1 parent 67f424f commit 0ab7e9c
Show file tree
Hide file tree
Showing 16 changed files with 462 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#
# TencentBlueKing is pleased to support the open source community by making
# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available.
# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
# Licensed under the MIT License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# http://opensource.org/licenses/MIT
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific language governing permissions and
# limitations under the License.
#
# We undertake not to change the open source license (MIT license) applicable
# to the current version of the project delivered to anyone in the future.
#
from datetime import datetime
from typing import Dict, Literal, Optional

from pydantic import BaseModel, Field


class ApiDebugHistoryRequest(BaseModel):
request_url: Optional[str] = Field(help="请求路由")
request_method: Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"] = Field(
"GET", help="HTTP 方法,默认为GET"
)
type: Literal["HTTP", "GRPC", "WEBSOCKET"] = Field("HTTP", help="请求类型,默认为HTTP")
authorization: Dict[str, str] = Field(None, help="认证信息")
path_params: Dict[str, str] = Field({}, help="路径参数")
query_params: Dict[str, str] = Field({}, help="查询参数")
body: Optional[str] = Field(None, help="请求体")
headers: Dict[str, str] = Field({}, help="请求头")
subpath: Optional[str] = Field(None, help="子路径")
use_test_app: bool = Field(False, help="是否使用测试应用")
use_user_from_cookies: bool = Field(False, help="是否使用 cookies 中的用户信息")
request_time: Optional[datetime] = Field(None, help="请求时间")
spec_version: Optional[int] = Field(1, help="请求版本")


class ApiDebugHistoryResponse(BaseModel):
status_code: Optional[int] = Field(500, help="返回结果的状态码")
proxy_time: float = Field(..., gt=0, help="处理时间,单位为秒,包含两位小数")
body: Optional[str] = Field(None)
spec_version: Optional[int] = Field(1, help="返回的结果版本")
error: Optional[str] = Field(None, help="错误信息")

# 格式化时间
def format_proxy_time(self) -> str:
return f"{self.proxy_time:.2f}"
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,11 @@ class APITestOutputSLZ(serializers.Serializer):
size = serializers.FloatField(help_text="响应体大小")
body = serializers.CharField(help_text="响应体内容")
headers = serializers.DictField(help_text="响应头")


class APIDebugHistoriesListOutputSLZ(serializers.Serializer):
id = serializers.IntegerField(read_only=True, help_text="测试历史ID")
gateway_id = serializers.IntegerField(read_only=True, help_text="网关ID")
resource_name = serializers.CharField(read_only=True, help_text="资源名称")
request = serializers.JSONField(help_text="请求参数")
response = serializers.JSONField(help_text="返回结果")
17 changes: 15 additions & 2 deletions src/dashboard/apigateway/apigateway/apis/web/api_test/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,23 @@
# We undertake not to change the open source license (MIT license) applicable
# to the current version of the project delivered to anyone in the future.
#
from django.urls import path
from django.urls import include, path

from .views import APITestApi
from .views import APIDebugHistoryListApi, APIDebugHistoryRetrieveDestroyApi, APITestApi

urlpatterns = [
path("", APITestApi.as_view(), name="api_test.tests"),
path(
"histories/",
include(
[
path("", APIDebugHistoryListApi.as_view(), name="api_debug.histories.list"),
path(
"<int:id>/",
APIDebugHistoryRetrieveDestroyApi.as_view(),
name="api_debug.histories.retrieve-destroy",
),
]
),
),
]
115 changes: 113 additions & 2 deletions src/dashboard/apigateway/apigateway/apis/web/api_test/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,37 @@
# We undertake not to change the open source license (MIT license) applicable
# to the current version of the project delivered to anyone in the future.
#
import time
from typing import Any, Dict

import requests
from django.conf import settings
from django.http import Http404
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes
from django.utils.translation import gettext as _
from drf_yasg.utils import swagger_auto_schema
from rest_framework import generics, status

from apigateway.apps.api_debug.constants import SPEC_VERSION
from apigateway.apps.api_debug.models import APIDebugHistory
from apigateway.biz.permission import ResourcePermissionHandler
from apigateway.biz.released_resource import get_released_resource_data
from apigateway.core.models import Stage
from apigateway.utils.curlify import to_curl
from apigateway.utils.responses import FailJsonResponse, OKJsonResponse
from apigateway.utils.time import convert_second_to_epoch_millisecond

from .data_models import ApiDebugHistoryRequest, ApiDebugHistoryResponse
from .prepared_request import PreparedRequestHeaders, PreparedRequestURL
from .serializers import APITestInputSLZ, APITestOutputSLZ
from .serializers import APIDebugHistoriesListOutputSLZ, APITestInputSLZ, APITestOutputSLZ

TEST_PERMISSION_EXPIRE_DAYS = 1


class APITestApi(generics.CreateAPIView):
@swagger_auto_schema(
request_body=APITestInputSLZ,
responses={status.HTTP_200_OK: APITestOutputSLZ},
operation_description="在线调试发起请求",
tags=["WebAPI.APITest"],
Expand Down Expand Up @@ -82,6 +87,26 @@ def post(self, request, *args, **kwargs):
stage_name=stage.name,
)

# 开始时间
start_time = time.perf_counter()
request_time = timezone.now()

# 入参检查
history_request = {
"request_url": prepared_request_url.request_url,
"request_method": data["method"],
"type": "HTTP",
"authorization": data.get("authorization", {}),
"path_params": data.get("path_params", {}),
"query_params": data.get("query_params", {}),
"body": data.get("body", ""),
"headers": data.get("headers", {}),
"subpath": data.get("subpath", ""),
"use_test_app": data.get("use_test_app", True),
"use_user_from_cookies": data.get("use_user_from_cookies", False),
"request_time": request_time,
"spec_version": SPEC_VERSION,
}
try:
response = requests.request(
method=data["method"],
Expand All @@ -96,7 +121,37 @@ def post(self, request, *args, **kwargs):
allow_redirects=False,
verify=False,
)
end_time = time.perf_counter()
proxy_time = end_time - start_time

# 结果检查
history_response = {
"status_code": response.status_code,
"response": response.text,
"proxy_time": proxy_time,
"spec_version": SPEC_VERSION,
}
success_history_data = {
"gateway": request.gateway,
"stage": stage,
"resource_name": released_resource.name,
"request": ApiDebugHistoryRequest(**history_request),
"response": ApiDebugHistoryResponse(**history_response),
}
APIDebugHistory.objects.create(**success_history_data)
except Exception as err:
# 结果检查
history_response = {
"error": err,
}
fail_history_data = {
"gateway": request.gateway,
"stage": stage,
"resource_name": released_resource.name,
"request": ApiDebugHistoryRequest(**history_request),
"response": ApiDebugHistoryResponse(**history_response),
}
APIDebugHistory.objects.create(**fail_history_data)
return FailJsonResponse(
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
code="UNKNOWN",
Expand Down Expand Up @@ -128,3 +183,59 @@ def _get_authorization_from_cookies(self) -> Dict[str, str]:
key: cookies.get(cookie_name, "")
for key, cookie_name in settings.BK_LOGIN_TICKET_KEY_TO_COOKIE_NAME.items()
}


class APIDebugHistoriesQuerySetMixin:
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(gateway=self.request.gateway)


@method_decorator(
name="get",
decorator=swagger_auto_schema(
operation_description="获取测试历史列表",
responses={status.HTTP_200_OK: APIDebugHistoriesListOutputSLZ(many=True)},
tags=["WebAPI.ResourceDebugHistory"],
),
)
class APIDebugHistoryListApi(APIDebugHistoriesQuerySetMixin, generics.ListAPIView):
queryset = APIDebugHistory.objects.order_by("-updated_time")
serializer_class = APIDebugHistoriesListOutputSLZ

def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
slz = APIDebugHistoriesListOutputSLZ(queryset, many=True)
return OKJsonResponse(data=slz.data)


@method_decorator(
name="get",
decorator=swagger_auto_schema(
operation_description="获取调用历史详情",
responses={status.HTTP_200_OK: APIDebugHistoriesListOutputSLZ()},
tags=["WebAPI.ResourceDebugHistory"],
),
)
@method_decorator(
name="delete",
decorator=swagger_auto_schema(
operation_description="删除调用历史",
responses={status.HTTP_204_NO_CONTENT: ""},
tags=["WebAPI.ResourceDebugHistory"],
),
)
class APIDebugHistoryRetrieveDestroyApi(APIDebugHistoriesQuerySetMixin, generics.RetrieveUpdateDestroyAPIView):
lookup_field = "id"
serializer_class = APIDebugHistoriesListOutputSLZ
queryset = APIDebugHistory.objects.all()

def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = APIDebugHistoriesListOutputSLZ(instance)
return OKJsonResponse(data=serializer.data)

def destroy(self, request, *args, **kwargs):
instance = self.get_object()
instance.delete()
return OKJsonResponse(status=status.HTTP_204_NO_CONTENT)
17 changes: 17 additions & 0 deletions src/dashboard/apigateway/apigateway/apps/api_debug/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# TencentBlueKing is pleased to support the open source community by making
# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available.
# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
# Licensed under the MIT License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# http://opensource.org/licenses/MIT
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific language governing permissions and
# limitations under the License.
#
# We undertake not to change the open source license (MIT license) applicable
# to the current version of the project delivered to anyone in the future.
#
18 changes: 18 additions & 0 deletions src/dashboard/apigateway/apigateway/apps/api_debug/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Register your models here.
#
# TencentBlueKing is pleased to support the open source community by making
# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available.
# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
# Licensed under the MIT License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# http://opensource.org/licenses/MIT
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific language governing permissions and
# limitations under the License.
#
# We undertake not to change the open source license (MIT license) applicable
# to the current version of the project delivered to anyone in the future.
#
22 changes: 22 additions & 0 deletions src/dashboard/apigateway/apigateway/apps/api_debug/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#
# TencentBlueKing is pleased to support the open source community by making
# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available.
# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
# Licensed under the MIT License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# http://opensource.org/licenses/MIT
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific language governing permissions and
# limitations under the License.
#
# We undertake not to change the open source license (MIT license) applicable
# to the current version of the project delivered to anyone in the future.
#
from django.apps import AppConfig


class PermissionConfig(AppConfig):
name = "apigateway.apps.api_debug"
20 changes: 20 additions & 0 deletions src/dashboard/apigateway/apigateway/apps/api_debug/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#
# TencentBlueKing is pleased to support the open source community by making
# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available.
# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
# Licensed under the MIT License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# http://opensource.org/licenses/MIT
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific language governing permissions and
# limitations under the License.
#
# We undertake not to change the open source license (MIT license) applicable
# to the current version of the project delivered to anyone in the future.
#
API_TEST_METHOD_CHOICES = [("HTTP", "HTTP"), ("GRPC", "GRPC"), ("WEBSOCKET", "WEBSOCKET")]

SPEC_VERSION = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 3.2.25 on 2024-07-26 02:51

from django.db import migrations, models
import django.db.models.deletion
import jsonfield.fields


class Migration(migrations.Migration):

initial = True

dependencies = [
('core', '0040_auto_20240517_1422'),
]

operations = [
migrations.CreateModel(
name='APIDebugHistory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_time', models.DateTimeField(auto_now_add=True, null=True)),
('updated_time', models.DateTimeField(auto_now=True, null=True)),
('created_by', models.CharField(blank=True, max_length=32, null=True)),
('updated_by', models.CharField(blank=True, max_length=32, null=True)),
('resource_name', models.CharField(help_text='资源名称', max_length=32)),
('request', jsonfield.fields.JSONField(blank=True, help_text='请求参数')),
('response', jsonfield.fields.JSONField(blank=True, help_text='返回结果')),
('gateway', models.ForeignKey(db_column='gateway_id', on_delete=django.db.models.deletion.CASCADE, to='core.gateway')),
('stage', models.ForeignKey(db_column='stage_id', on_delete=django.db.models.deletion.CASCADE, to='core.stage')),
],
options={
'verbose_name': 'APIDebugHistory',
'verbose_name_plural': 'APIDebugHistory',
'db_table': 'api_debug_history',
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# TencentBlueKing is pleased to support the open source community by making
# 蓝鲸智云 - API 网关(BlueKing - APIGateway) available.
# Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
# Licensed under the MIT License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# http://opensource.org/licenses/MIT
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific language governing permissions and
# limitations under the License.
#
# We undertake not to change the open source license (MIT license) applicable
# to the current version of the project delivered to anyone in the future.
#
Loading

0 comments on commit 0ab7e9c

Please sign in to comment.