Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:api_test history #759

Merged
merged 11 commits into from
Jul 29, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -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
@@ -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
@@ -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"],
@@ -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"],
@@ -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),
}
Lawrence-lkq marked this conversation as resolved.
Show resolved Hide resolved
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(
Lawrence-lkq marked this conversation as resolved.
Show resolved Hide resolved
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
code="UNKNOWN",
@@ -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