algo-backend-framework 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- algo_backend/__init__.py +0 -0
- algo_backend/config/__init__.py +8 -0
- algo_backend/config/basic_config.py +13 -0
- algo_backend/config/loguru_config.py +19 -0
- algo_backend/exception/__init__.py +22 -0
- algo_backend/exception/error_code_manage.py +126 -0
- algo_backend/exception/exception.py +42 -0
- algo_backend/exception/status_code.py +103 -0
- algo_backend/handler/__init__.py +3 -0
- algo_backend/handler/exception_to_vo.py +37 -0
- algo_backend/handler/operation_handler.py +71 -0
- algo_backend/intercept/__init__.py +9 -0
- algo_backend/intercept/common.py +45 -0
- algo_backend/intercept/http.py +40 -0
- algo_backend/intercept/validate.py +78 -0
- algo_backend/log/__init__.py +1 -0
- algo_backend/log/common.py +16 -0
- algo_backend/log/loguru/__init__.py +5 -0
- algo_backend/log/loguru/log_clean.py +140 -0
- algo_backend/log/loguru/log_setup.py +89 -0
- algo_backend/log/loguru/log_starter.py +65 -0
- algo_backend/log/loguru/patch_logging.py +83 -0
- algo_backend/log/nblog/__init__.py +0 -0
- algo_backend/metrics/__init__.py +22 -0
- algo_backend/metrics/collector/__init__.py +12 -0
- algo_backend/metrics/collector/common.py +17 -0
- algo_backend/metrics/collector/gc_metrics.py +74 -0
- algo_backend/metrics/collector/schedule_monitor.py +50 -0
- algo_backend/metrics/collector/system_metrics.py +169 -0
- algo_backend/metrics/http_metrics.py +56 -0
- algo_backend/metrics/prometheus_context.py +55 -0
- algo_backend/metrics/time_cost_metrics.py +146 -0
- algo_backend/middleware/__init__.py +4 -0
- algo_backend/middleware/cors.py +10 -0
- algo_backend/middleware/metrics.py +12 -0
- algo_backend/schema/__init__.py +3 -0
- algo_backend/schema/vo.py +83 -0
- algo_backend/starter/__init__.py +4 -0
- algo_backend/starter/default_app_generator.py +169 -0
- algo_backend/starter/default_service_starter.py +70 -0
- algo_backend/starter/event_list.py +32 -0
- algo_backend/utils/__init__.py +8 -0
- algo_backend/utils/meta_class.py +50 -0
- algo_backend/utils/utils.py +22 -0
- algo_backend_framework-0.0.1.dist-info/METADATA +60 -0
- algo_backend_framework-0.0.1.dist-info/RECORD +48 -0
- algo_backend_framework-0.0.1.dist-info/WHEEL +5 -0
- algo_backend_framework-0.0.1.dist-info/top_level.txt +1 -0
algo_backend/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from algo_backend.utils import OsAttrMeta
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ErrorCodeConfig:
|
|
7
|
+
SERVICE_PREFIX: int = os.getenv("ERROR_CODE_SERVICE_PREFIX", "0")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ServiceConfig(metaclass=OsAttrMeta):
|
|
11
|
+
HTTP_PORT: int = 8100
|
|
12
|
+
TIMEOUT_KEEP_ALIVE: int = 1000
|
|
13
|
+
PROCESS_NUM: int = 1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from algo_backend.utils import OsAttrMeta
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LoguruConfig(metaclass=OsAttrMeta):
|
|
7
|
+
LOGGER_PATH: str = "/logger"
|
|
8
|
+
LOG_RETENTION_DAY: int = 60
|
|
9
|
+
DISABLE_LOG_PKG: str = ""
|
|
10
|
+
LOG_ADD_CONTAINED_ID: bool = False
|
|
11
|
+
SAVE_INFO_LEVEL: bool = False
|
|
12
|
+
SAVE_DEBUG_LOG: bool = True
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def get_disable_log_pkg(cls) -> List[str]:
|
|
16
|
+
if cls.DISABLE_LOG_PKG:
|
|
17
|
+
return cls.DISABLE_LOG_PKG.split(",")
|
|
18
|
+
else:
|
|
19
|
+
return []
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .error_code_manage import ApiErrorCodeManage, BasicCodeManage
|
|
2
|
+
from .exception import BasicApiInnerException, BasicCommonException, BasicException
|
|
3
|
+
from .status_code import (
|
|
4
|
+
BasicApiId,
|
|
5
|
+
BasicApiInnerErrorCode,
|
|
6
|
+
BasicStatusCode,
|
|
7
|
+
CommonStatusCode,
|
|
8
|
+
DefaultApiErrorCode,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BasicStatusCode",
|
|
13
|
+
"CommonStatusCode",
|
|
14
|
+
"BasicApiId",
|
|
15
|
+
"BasicApiInnerErrorCode",
|
|
16
|
+
"BasicApiInnerException",
|
|
17
|
+
"ApiErrorCodeManage",
|
|
18
|
+
"BasicCodeManage",
|
|
19
|
+
"BasicCommonException",
|
|
20
|
+
"DefaultApiErrorCode",
|
|
21
|
+
"BasicException",
|
|
22
|
+
]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from algo_backend.config import ErrorCodeConfig
|
|
6
|
+
|
|
7
|
+
from . import status_code
|
|
8
|
+
from .status_code import BasicApiId, BasicApiInnerErrorCode, BasicStatusCode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApiErrorCodeManage:
|
|
12
|
+
"""
|
|
13
|
+
构建”5aaabb“错误码,aaa接口id,bb表示接口内部错误码
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def set_error_code_prefix_env(cls, prefix: Optional[int] = None):
|
|
18
|
+
"""
|
|
19
|
+
服务启动时执行这个函数
|
|
20
|
+
也可以启动服务时通过环境变量ERROR_CODE_SERVICE_PREFIX指定服务前缀
|
|
21
|
+
如果输入的prefix code不符合规则,则忽略,保持原来的6位
|
|
22
|
+
"""
|
|
23
|
+
if prefix and 1 <= prefix <= 99:
|
|
24
|
+
ErrorCodeConfig.SERVICE_PREFIX = prefix
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def scan_module_and_summary(cls, *list_module) -> list:
|
|
28
|
+
"""扫描模块中的ApiId和错误码枚举类"""
|
|
29
|
+
api_id_dict: dict = {}
|
|
30
|
+
api_err_code_info = []
|
|
31
|
+
|
|
32
|
+
cls_list = []
|
|
33
|
+
for m in list_module:
|
|
34
|
+
cls_list.extend(
|
|
35
|
+
[obj for name, obj in inspect.getmembers(m) if inspect.isclass(obj)]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
for obj in set(cls_list):
|
|
39
|
+
# 查找继承自BasicApiId的枚举类
|
|
40
|
+
if issubclass(obj, BasicApiId) and obj != BasicApiId:
|
|
41
|
+
for enum_member in obj:
|
|
42
|
+
api_id_dict[enum_member.value] = enum_member.name
|
|
43
|
+
api_err_code_info.append(
|
|
44
|
+
[
|
|
45
|
+
enum_member.value,
|
|
46
|
+
BasicApiInnerErrorCode.gen_standard_code(enum_member, 0),
|
|
47
|
+
"",
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# 查找继承自BasicApiInnerErrorCode的枚举类
|
|
52
|
+
elif (
|
|
53
|
+
issubclass(obj, BasicApiInnerErrorCode)
|
|
54
|
+
and obj != BasicApiInnerErrorCode
|
|
55
|
+
and hasattr(obj, "__api_id__")
|
|
56
|
+
):
|
|
57
|
+
api_id_enum: BasicApiId = getattr(obj, "__api_id__")
|
|
58
|
+
for enum_member in obj:
|
|
59
|
+
api_err_code_info.append(
|
|
60
|
+
[
|
|
61
|
+
api_id_enum.value,
|
|
62
|
+
BasicApiInnerErrorCode.gen_standard_code(
|
|
63
|
+
api_id_enum, enum_member.value
|
|
64
|
+
),
|
|
65
|
+
enum_member.msg,
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
for idx, (api_code, _, _) in enumerate(api_err_code_info):
|
|
70
|
+
api_err_code_info[idx][0] = api_id_dict[api_code]
|
|
71
|
+
|
|
72
|
+
# 排序
|
|
73
|
+
api_err_code_info.sort(key=lambda x: x[1])
|
|
74
|
+
|
|
75
|
+
return api_err_code_info
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def summary_module_api_error_code_markdown(cls, *list_module):
|
|
79
|
+
"""
|
|
80
|
+
打印模块中的ApiId和错误码枚举类
|
|
81
|
+
"""
|
|
82
|
+
result = cls.scan_module_and_summary(*list_module)
|
|
83
|
+
|
|
84
|
+
print("| ApiName | ErrorCode | Message |")
|
|
85
|
+
print("|--------|------------|---------|")
|
|
86
|
+
for api_id, api_code, msg in result:
|
|
87
|
+
print(f"| {api_id} | {api_code} | {msg} |")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class BasicCodeManage:
|
|
91
|
+
__DEFAULT_MODULE = sys.modules[status_code.__name__]
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def scan_module_and_summary(cls, *list_module) -> list:
|
|
95
|
+
"""扫描模块中BasicStatusCode枚举类及其子类"""
|
|
96
|
+
if cls.__DEFAULT_MODULE not in list_module:
|
|
97
|
+
list_module = list_module + (cls.__DEFAULT_MODULE,)
|
|
98
|
+
|
|
99
|
+
result = []
|
|
100
|
+
cls_list = []
|
|
101
|
+
|
|
102
|
+
for m in list_module:
|
|
103
|
+
cls_list.extend(
|
|
104
|
+
[obj for name, obj in inspect.getmembers(m) if inspect.isclass(obj)]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
for obj in set(cls_list):
|
|
108
|
+
if issubclass(obj, BasicStatusCode) and obj != BasicApiId:
|
|
109
|
+
for enum_member in obj:
|
|
110
|
+
result.append([enum_member.value, enum_member.msg])
|
|
111
|
+
|
|
112
|
+
result.sort(key=lambda x: x[0])
|
|
113
|
+
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def summary_error_code_markdown(cls, *list_module):
|
|
118
|
+
"""
|
|
119
|
+
打印模块中的ApiId和错误码枚举类
|
|
120
|
+
"""
|
|
121
|
+
result = cls.scan_module_and_summary(*list_module)
|
|
122
|
+
|
|
123
|
+
print("| Code | Message |")
|
|
124
|
+
print("|--------|------------|")
|
|
125
|
+
for code, msg in result:
|
|
126
|
+
print(f"| {code} | {msg} |")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
|
|
3
|
+
from .status_code import BasicApiId, BasicApiInnerErrorCode, BasicStatusCode
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BasicException(Exception):
|
|
7
|
+
"""
|
|
8
|
+
基础异常类
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, code: int, msg: str):
|
|
12
|
+
super().__init__(msg)
|
|
13
|
+
self.code = code
|
|
14
|
+
self.msg = msg
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# 定义泛型变量 T,限定为 BasicStatusCode 的子类
|
|
18
|
+
T = TypeVar("T", bound=BasicStatusCode)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BasicCommonException(BasicException):
|
|
22
|
+
def __init__(self, status_code: T, **kwargs):
|
|
23
|
+
super().__init__(code=status_code.code, msg=status_code.msg.format(**kwargs))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# 定义泛型变量 ET,限定为 BasicApiInnerErrorCode 的子类
|
|
27
|
+
ET = TypeVar("ET", bound=BasicApiInnerErrorCode)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BasicApiInnerException(BasicException):
|
|
31
|
+
"""
|
|
32
|
+
接口内部异常类的基类
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, status_code: ET, **kwargs):
|
|
36
|
+
super().__init__(code=status_code.code, msg=status_code.msg.format(**kwargs))
|
|
37
|
+
|
|
38
|
+
def add_api_id(self, api_id: BasicApiId) -> "BasicApiInnerException":
|
|
39
|
+
self.code = BasicApiInnerErrorCode.gen_standard_code(
|
|
40
|
+
api_id=api_id, inner_error_code=self.code
|
|
41
|
+
)
|
|
42
|
+
return self
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from algo_backend.config import ErrorCodeConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def add_service_prefix(v: int):
|
|
7
|
+
"""
|
|
8
|
+
对于失败的状态码,添加服务号码前缀
|
|
9
|
+
"""
|
|
10
|
+
v_str = str(v)
|
|
11
|
+
if len(v_str) == 8:
|
|
12
|
+
return v
|
|
13
|
+
if v is not None and v != 0 and ErrorCodeConfig.SERVICE_PREFIX:
|
|
14
|
+
v = int(f"{ErrorCodeConfig.SERVICE_PREFIX}{v_str}")
|
|
15
|
+
return v
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BasicStatusCode(Enum):
|
|
19
|
+
def __new__(cls, value: int, msg: str):
|
|
20
|
+
assert 0 <= value <= 999999, f"错误码长度不能超过6位,且非负数,{value}"
|
|
21
|
+
obj = object.__new__(cls)
|
|
22
|
+
obj._value_ = obj
|
|
23
|
+
obj._code = value
|
|
24
|
+
obj.msg = msg
|
|
25
|
+
return obj
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def code(self):
|
|
29
|
+
return add_service_prefix(self._code)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def value(self):
|
|
33
|
+
return self.code
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CommonStatusCode(BasicStatusCode):
|
|
37
|
+
SUCCESS = (0, "成功")
|
|
38
|
+
|
|
39
|
+
ERROR_REQ_URL = (400001, "请求路径错误")
|
|
40
|
+
ERROR_REQ_METHOD = (400002, "{url}请求方法错误")
|
|
41
|
+
BODY_EMPTY_ERR = (400003, "{url}请求体内容为空")
|
|
42
|
+
PARAM_ERROR = (400006, "接口{url}参数错误,{msg}")
|
|
43
|
+
ERROR_HEADER_NOW_EXISTS = (400007, "接口{url}缺少请求头:{header}")
|
|
44
|
+
|
|
45
|
+
UNKNOWN_ERROR = (500000, "未知错误: {msg}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BasicApiId(Enum):
|
|
49
|
+
"""
|
|
50
|
+
接口编号枚举类
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __new__(cls, value: int):
|
|
54
|
+
assert 0 <= value <= 999, f"接口编号长度不能超过3位,且非负数,{value}"
|
|
55
|
+
obj = object.__new__(cls)
|
|
56
|
+
obj._value_ = obj
|
|
57
|
+
obj.code = value
|
|
58
|
+
return obj
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def value(self):
|
|
62
|
+
return self.code
|
|
63
|
+
|
|
64
|
+
def bind_api(self):
|
|
65
|
+
"""
|
|
66
|
+
装饰器,绑定绑定api id
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def decorator(enum_class):
|
|
70
|
+
enum_class.__api_id__ = self
|
|
71
|
+
return enum_class
|
|
72
|
+
|
|
73
|
+
return decorator
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DefaultApiErrorCode(BasicApiId):
|
|
77
|
+
"""
|
|
78
|
+
0表示不提供任何ApiId
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
DEFAULT_ERROR = 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class BasicApiInnerErrorCode(Enum):
|
|
85
|
+
def __new__(cls, value: int, msg: str):
|
|
86
|
+
assert 1 <= value <= 99, f"接口内部错误码长度不能超过2位,且为正数,{value}"
|
|
87
|
+
obj = object.__new__(cls)
|
|
88
|
+
obj._value_ = obj
|
|
89
|
+
obj.code = value
|
|
90
|
+
obj.msg = msg
|
|
91
|
+
return obj
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def value(self):
|
|
95
|
+
return self.code
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def gen_standard_code(cls, api_id: BasicApiId, inner_error_code: int = 0):
|
|
99
|
+
"""
|
|
100
|
+
拼接并生成6位错误码
|
|
101
|
+
"""
|
|
102
|
+
api_id = api_id or DefaultApiErrorCode.DEFAULT_ERROR
|
|
103
|
+
return 500000 + api_id.code * 100 + inner_error_code
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from algo_backend.exception import (
|
|
5
|
+
BasicApiId,
|
|
6
|
+
BasicApiInnerException,
|
|
7
|
+
BasicCommonException,
|
|
8
|
+
CommonStatusCode,
|
|
9
|
+
)
|
|
10
|
+
from algo_backend.schema import AbstractRespVo
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def gen_vo_from_exception(
|
|
16
|
+
vo_cls: type(AbstractRespVo),
|
|
17
|
+
e: Exception,
|
|
18
|
+
api_name: Optional[str] = None,
|
|
19
|
+
request_id: Optional[str] = None,
|
|
20
|
+
api_id: Optional[BasicApiId] = None,
|
|
21
|
+
) -> AbstractRespVo:
|
|
22
|
+
"""
|
|
23
|
+
将异常转换为vo
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
_params = dict(api_name=api_name, request_id=request_id)
|
|
27
|
+
if isinstance(e, BasicCommonException):
|
|
28
|
+
vo = vo_cls.from_exception(e, **_params)
|
|
29
|
+
elif isinstance(e, BasicApiInnerException):
|
|
30
|
+
vo = vo_cls.from_exception(e.add_api_id(api_id=api_id), **_params)
|
|
31
|
+
else:
|
|
32
|
+
vo = vo_cls.from_exception(
|
|
33
|
+
BasicCommonException(CommonStatusCode.UNKNOWN_ERROR, msg=str(e)),
|
|
34
|
+
**_params,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return vo
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import traceback
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Awaitable, Callable, get_type_hints
|
|
6
|
+
|
|
7
|
+
from algo_backend.exception import (
|
|
8
|
+
BasicApiId,
|
|
9
|
+
DefaultApiErrorCode,
|
|
10
|
+
)
|
|
11
|
+
from algo_backend.metrics import PrometheusTimeCostMetricSetting
|
|
12
|
+
from algo_backend.schema import AbstractRespVo
|
|
13
|
+
from .exception_to_vo import gen_vo_from_exception
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def timing_and_exception_handler(
|
|
19
|
+
func=None,
|
|
20
|
+
*,
|
|
21
|
+
api_id: BasicApiId = DefaultApiErrorCode.DEFAULT_ERROR,
|
|
22
|
+
api_name: str = "",
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
装饰器:用于统计函数执行时间并捕获异常
|
|
26
|
+
函数中需要包含参数reqid或者request_id
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def decorator(
|
|
30
|
+
func: Callable[..., Awaitable[AbstractRespVo]],
|
|
31
|
+
) -> Callable[..., Awaitable[AbstractRespVo]]:
|
|
32
|
+
@wraps(func)
|
|
33
|
+
async def wrapper(*args, **kwargs):
|
|
34
|
+
start_time = time.perf_counter()
|
|
35
|
+
|
|
36
|
+
request_id = kwargs.get("request_id", None) or kwargs.get("reqid", None)
|
|
37
|
+
_name = func.__name__ or api_name
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
# 执行原函数
|
|
41
|
+
logger.info(f"ReqId: {request_id} | Function: {_name} | Start")
|
|
42
|
+
result: AbstractRespVo = await func(*args, **kwargs)
|
|
43
|
+
# 计算耗时
|
|
44
|
+
elapsed_time = time.perf_counter() - start_time
|
|
45
|
+
logger.info(
|
|
46
|
+
f"ReqId: {request_id} | Function: {_name} | COST:{elapsed_time:.4f}s"
|
|
47
|
+
)
|
|
48
|
+
PrometheusTimeCostMetricSetting.api_metrics_instance().add(
|
|
49
|
+
_name, elapsed_time
|
|
50
|
+
)
|
|
51
|
+
return result.set_request_id(request_id)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
# 计算耗时
|
|
54
|
+
elapsed_time = time.perf_counter() - start_time
|
|
55
|
+
PrometheusTimeCostMetricSetting.api_metrics_instance().add_error(_name)
|
|
56
|
+
# 记录异常信息和完整堆栈
|
|
57
|
+
logger.error(
|
|
58
|
+
f"ReqId: {request_id} | Function: {_name} | COST:{elapsed_time:.4f}s | Exception: {str(e)}\n"
|
|
59
|
+
f"Traceback:\n{traceback.format_exc()}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
vo_cls: type(AbstractRespVo) = get_type_hints(func).get(
|
|
63
|
+
"return"
|
|
64
|
+
) # 这里可能会失败,因为无法强制用户的类型
|
|
65
|
+
return gen_vo_from_exception(
|
|
66
|
+
vo_cls, e, api_name=_name, request_id=request_id, api_id=api_id
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return wrapper
|
|
70
|
+
|
|
71
|
+
return decorator if func is None else decorator(func)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Dict, Optional
|
|
3
|
+
|
|
4
|
+
from starlette.requests import Request
|
|
5
|
+
|
|
6
|
+
from algo_backend.schema import AbstractRespVo, BaseRespVo
|
|
7
|
+
from algo_backend.utils.utils import gen_random_request_id
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BasicExceptionInterceptor(ABC):
|
|
11
|
+
"""
|
|
12
|
+
拦截接口关于schema检验的报错,并返回约定的body
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
default_vo_type: type(AbstractRespVo) = BaseRespVo,
|
|
18
|
+
url_vo_dict: Optional[Dict[str, type(AbstractRespVo)]] = None,
|
|
19
|
+
):
|
|
20
|
+
self.default_vo_type: type(AbstractRespVo) = default_vo_type
|
|
21
|
+
# url和响应的匹配策略
|
|
22
|
+
self.url_vo_dict = url_vo_dict or {}
|
|
23
|
+
|
|
24
|
+
def get_vo_type(self, url: str) -> type(AbstractRespVo):
|
|
25
|
+
"""
|
|
26
|
+
用户可以继承BasicExceptionInterceptor的子类,覆盖这个方法从而实现更加复杂的url和响应体匹配策略
|
|
27
|
+
:param url:
|
|
28
|
+
:return:
|
|
29
|
+
"""
|
|
30
|
+
return self.url_vo_dict.get(url, self.default_vo_type)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def extract_url(cls, request: Request):
|
|
34
|
+
try:
|
|
35
|
+
url = request.url.path
|
|
36
|
+
except Exception:
|
|
37
|
+
url = ""
|
|
38
|
+
return url
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def get_request_id(cls, request: Request):
|
|
42
|
+
return request.headers.get("x-request-id", gen_random_request_id())
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def intercept(self, request: Request, exc: Exception): ...
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from fastapi.exceptions import StarletteHTTPException
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
from starlette.requests import Request
|
|
6
|
+
|
|
7
|
+
from algo_backend.exception import BasicCommonException, CommonStatusCode
|
|
8
|
+
|
|
9
|
+
from .common import BasicExceptionInterceptor
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HTTPExceptionInterceptor(BasicExceptionInterceptor):
|
|
15
|
+
"""
|
|
16
|
+
拦截接口关于http异常的报错,并返回约定的body
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, **kwargs):
|
|
20
|
+
super().__init__(**kwargs)
|
|
21
|
+
|
|
22
|
+
def intercept(self, request: Request, exc: StarletteHTTPException):
|
|
23
|
+
url: str = self.extract_url(request)
|
|
24
|
+
reqid: str = self.get_request_id(request)
|
|
25
|
+
|
|
26
|
+
logger.info(
|
|
27
|
+
f"req_id: {url} | 错误码: {exc.status_code} | 错误信息: {exc.detail}"
|
|
28
|
+
)
|
|
29
|
+
if exc.status_code == 405:
|
|
30
|
+
e = BasicCommonException(CommonStatusCode.ERROR_REQ_METHOD, url=url)
|
|
31
|
+
elif exc.status_code == 404:
|
|
32
|
+
e = BasicCommonException(CommonStatusCode.ERROR_REQ_URL, url=url)
|
|
33
|
+
else:
|
|
34
|
+
e = BasicCommonException(
|
|
35
|
+
CommonStatusCode.UNKNOWN_ERROR, msg=f"{exc.status_code},{exc.detail}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
vo = self.get_vo_type(url).from_exception(e, request_id=reqid)
|
|
39
|
+
|
|
40
|
+
return JSONResponse(content=vo.model_dump(), status_code=200)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from fastapi.exceptions import RequestValidationError
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
from starlette.requests import Request
|
|
6
|
+
|
|
7
|
+
from algo_backend.exception import BasicCommonException, CommonStatusCode
|
|
8
|
+
|
|
9
|
+
from .common import BasicExceptionInterceptor
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ValidateExceptionInterceptor(BasicExceptionInterceptor):
|
|
15
|
+
"""
|
|
16
|
+
拦截接口关于schema检验的报错,并返回约定的body
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, exc_msg_len=None, **kwargs):
|
|
20
|
+
super().__init__(**kwargs)
|
|
21
|
+
self.exc_msg_len: int = exc_msg_len or 600
|
|
22
|
+
|
|
23
|
+
def intercept(self, request: Request, exc: RequestValidationError) -> JSONResponse:
|
|
24
|
+
"""
|
|
25
|
+
拦截接口关于schema检验的报错,并返回约定的body
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
reqid: str = self.get_request_id(request)
|
|
29
|
+
url: str = self.extract_url(request)
|
|
30
|
+
|
|
31
|
+
logger.error(
|
|
32
|
+
f"Reqid: {reqid} | 请求体参数错误: {str(exc)[: self.exc_msg_len]}..."
|
|
33
|
+
)
|
|
34
|
+
if int(request.headers.get("content-length")) == 0 and request.method in (
|
|
35
|
+
"POST",
|
|
36
|
+
"PUT",
|
|
37
|
+
"PATCH",
|
|
38
|
+
):
|
|
39
|
+
e = BasicCommonException(
|
|
40
|
+
CommonStatusCode.BODY_EMPTY_ERR,
|
|
41
|
+
url=url,
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
reason: str = self.parse_request_validation_error(exc)
|
|
45
|
+
e = BasicCommonException(
|
|
46
|
+
CommonStatusCode.PARAM_ERROR,
|
|
47
|
+
url=url,
|
|
48
|
+
msg=reason,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
vo = self.get_vo_type(url).from_exception(e, request_id=reqid)
|
|
52
|
+
|
|
53
|
+
return JSONResponse(
|
|
54
|
+
status_code=200, # 或其他合适的 HTTP 状态码
|
|
55
|
+
content=vo.model_dump(), # 将 Pydantic 模型转换为字典
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def parse_request_validation_error(exc: RequestValidationError):
|
|
60
|
+
"""
|
|
61
|
+
报错润色
|
|
62
|
+
RequestValidationError([{'type': 'string_type', 'loc': ('body', 'messages', 0, 'role'), 'msg': 'Input should be a valid string...[]},
|
|
63
|
+
{'type': 'string_type', 'loc': ('body', 'messages', 0, 'content'), 'msg': 'Input should be a valid string', 'input': []}])
|
|
64
|
+
转换结果如下:
|
|
65
|
+
'请求参数错误,发现2个字段问题:messages[0].role: Input should be a valid string;messages[0].content: Input should be a valid string'
|
|
66
|
+
"""
|
|
67
|
+
error_fields = []
|
|
68
|
+
for error in exc.errors():
|
|
69
|
+
loc = error["loc"]
|
|
70
|
+
# 忽略第一个元素(通常是 body)
|
|
71
|
+
field_path = ".".join(
|
|
72
|
+
f"[{i}]" if isinstance(i, int) else i for i in loc[1:]
|
|
73
|
+
).replace(".[", "[")
|
|
74
|
+
msg = error["msg"]
|
|
75
|
+
error_fields.append(f"{field_path}: {msg}")
|
|
76
|
+
return f"请求参数错误,发现{len(error_fields)}个字段问题:" + ";".join(
|
|
77
|
+
error_fields
|
|
78
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .common import BasicLogStarter
|