algo-backend-framework 0.0.1__tar.gz
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_framework-0.0.1/PKG-INFO +60 -0
- algo_backend_framework-0.0.1/README.md +46 -0
- algo_backend_framework-0.0.1/algo_backend/__init__.py +0 -0
- algo_backend_framework-0.0.1/algo_backend/config/__init__.py +8 -0
- algo_backend_framework-0.0.1/algo_backend/config/basic_config.py +13 -0
- algo_backend_framework-0.0.1/algo_backend/config/loguru_config.py +19 -0
- algo_backend_framework-0.0.1/algo_backend/exception/__init__.py +22 -0
- algo_backend_framework-0.0.1/algo_backend/exception/error_code_manage.py +126 -0
- algo_backend_framework-0.0.1/algo_backend/exception/exception.py +42 -0
- algo_backend_framework-0.0.1/algo_backend/exception/status_code.py +103 -0
- algo_backend_framework-0.0.1/algo_backend/handler/__init__.py +3 -0
- algo_backend_framework-0.0.1/algo_backend/handler/exception_to_vo.py +37 -0
- algo_backend_framework-0.0.1/algo_backend/handler/operation_handler.py +71 -0
- algo_backend_framework-0.0.1/algo_backend/intercept/__init__.py +9 -0
- algo_backend_framework-0.0.1/algo_backend/intercept/common.py +45 -0
- algo_backend_framework-0.0.1/algo_backend/intercept/http.py +40 -0
- algo_backend_framework-0.0.1/algo_backend/intercept/validate.py +78 -0
- algo_backend_framework-0.0.1/algo_backend/log/__init__.py +1 -0
- algo_backend_framework-0.0.1/algo_backend/log/common.py +16 -0
- algo_backend_framework-0.0.1/algo_backend/log/loguru/__init__.py +5 -0
- algo_backend_framework-0.0.1/algo_backend/log/loguru/log_clean.py +140 -0
- algo_backend_framework-0.0.1/algo_backend/log/loguru/log_setup.py +89 -0
- algo_backend_framework-0.0.1/algo_backend/log/loguru/log_starter.py +65 -0
- algo_backend_framework-0.0.1/algo_backend/log/loguru/patch_logging.py +83 -0
- algo_backend_framework-0.0.1/algo_backend/log/nblog/__init__.py +0 -0
- algo_backend_framework-0.0.1/algo_backend/metrics/__init__.py +22 -0
- algo_backend_framework-0.0.1/algo_backend/metrics/collector/__init__.py +12 -0
- algo_backend_framework-0.0.1/algo_backend/metrics/collector/common.py +17 -0
- algo_backend_framework-0.0.1/algo_backend/metrics/collector/gc_metrics.py +74 -0
- algo_backend_framework-0.0.1/algo_backend/metrics/collector/schedule_monitor.py +50 -0
- algo_backend_framework-0.0.1/algo_backend/metrics/collector/system_metrics.py +169 -0
- algo_backend_framework-0.0.1/algo_backend/metrics/http_metrics.py +56 -0
- algo_backend_framework-0.0.1/algo_backend/metrics/prometheus_context.py +55 -0
- algo_backend_framework-0.0.1/algo_backend/metrics/time_cost_metrics.py +146 -0
- algo_backend_framework-0.0.1/algo_backend/middleware/__init__.py +4 -0
- algo_backend_framework-0.0.1/algo_backend/middleware/cors.py +10 -0
- algo_backend_framework-0.0.1/algo_backend/middleware/metrics.py +12 -0
- algo_backend_framework-0.0.1/algo_backend/schema/__init__.py +3 -0
- algo_backend_framework-0.0.1/algo_backend/schema/vo.py +83 -0
- algo_backend_framework-0.0.1/algo_backend/starter/__init__.py +4 -0
- algo_backend_framework-0.0.1/algo_backend/starter/default_app_generator.py +169 -0
- algo_backend_framework-0.0.1/algo_backend/starter/default_service_starter.py +70 -0
- algo_backend_framework-0.0.1/algo_backend/starter/event_list.py +32 -0
- algo_backend_framework-0.0.1/algo_backend/utils/__init__.py +8 -0
- algo_backend_framework-0.0.1/algo_backend/utils/meta_class.py +50 -0
- algo_backend_framework-0.0.1/algo_backend/utils/utils.py +22 -0
- algo_backend_framework-0.0.1/algo_backend_framework.egg-info/PKG-INFO +60 -0
- algo_backend_framework-0.0.1/algo_backend_framework.egg-info/SOURCES.txt +51 -0
- algo_backend_framework-0.0.1/algo_backend_framework.egg-info/dependency_links.txt +1 -0
- algo_backend_framework-0.0.1/algo_backend_framework.egg-info/requires.txt +7 -0
- algo_backend_framework-0.0.1/algo_backend_framework.egg-info/top_level.txt +1 -0
- algo_backend_framework-0.0.1/pyproject.toml +28 -0
- algo_backend_framework-0.0.1/setup.cfg +4 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: algo-backend-framework
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Ctcdn algorithm backend framework
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fastapi>=0.128.0
|
|
8
|
+
Requires-Dist: loguru>=0.7.3
|
|
9
|
+
Requires-Dist: prometheus-client>=0.24.1
|
|
10
|
+
Requires-Dist: psutil>=7.2.1
|
|
11
|
+
Requires-Dist: pydantic>=2.12.5
|
|
12
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
13
|
+
Requires-Dist: uvicorn>=0.40.0
|
|
14
|
+
|
|
15
|
+
# algo-backend-framework-python
|
|
16
|
+
|
|
17
|
+
# 功能概述
|
|
18
|
+
- 配置
|
|
19
|
+
- 错误码、异常、参数检验报错转换
|
|
20
|
+
- 标准响应体、x-request-id
|
|
21
|
+
- 日志框架支持:loguru、nb_log
|
|
22
|
+
- 多进程prometheus指标
|
|
23
|
+
- otel
|
|
24
|
+
|
|
25
|
+
## 分支规范
|
|
26
|
+
- master分支: 归档分支
|
|
27
|
+
- release/release-×.×.×: 发布分支,×.×.×为版本号;修改[pyproject.toml](pyproject.toml)中的version构建并发布包
|
|
28
|
+
- dev分支: 开发分支;合并至release分支时,需要提mr,mr需要填写版本号和功能描述;需要充分自测和review之后才能合并至release
|
|
29
|
+
- feat/feat-×××××: 功能分支;合并至dev时需要mr,mr需要填写功能名称和功能描述;新功能或较大重构需要建feat分支,较小的改动不必开feat分支
|
|
30
|
+
|
|
31
|
+
## 本地调试
|
|
32
|
+
|
|
33
|
+
本地开发需要打包验证,则先构建包(但不要发布),然后在本地目录离线安装
|
|
34
|
+
```shell
|
|
35
|
+
# 构建包
|
|
36
|
+
uv build
|
|
37
|
+
# 拷贝wheel包到本地测试目录下执行以下命令
|
|
38
|
+
uv add algo_backend_framework-*.*.*-py3-none-any.whl
|
|
39
|
+
# 调试时发现包有问题,只需要remove后重新打包,并重新add
|
|
40
|
+
uv remove algo-backend-framework
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 开发约定
|
|
44
|
+
- 代码提交前进行sonar扫描和语法检查,并进行代码格式化
|
|
45
|
+
```
|
|
46
|
+
uv run ruff format algo_backend/×××××
|
|
47
|
+
```
|
|
48
|
+
- 子模块__init__.py文件需要声明包内暴露模块,不建议用户通过包内文件名导入,以减少后期微调重构引起的不兼容
|
|
49
|
+
- 跨模块导入一律使用`from algo_backend.× import 模块名`的全限定包名,同级目录下导入从使用`from . import `
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## 发布
|
|
53
|
+
- 在release分支进行构建和推送
|
|
54
|
+
```shell
|
|
55
|
+
uv build
|
|
56
|
+
# 推送ctcdn nexus
|
|
57
|
+
uv publish --index ctcdn-pypi-upload
|
|
58
|
+
# 推送pypi
|
|
59
|
+
uv publish
|
|
60
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# algo-backend-framework-python
|
|
2
|
+
|
|
3
|
+
# 功能概述
|
|
4
|
+
- 配置
|
|
5
|
+
- 错误码、异常、参数检验报错转换
|
|
6
|
+
- 标准响应体、x-request-id
|
|
7
|
+
- 日志框架支持:loguru、nb_log
|
|
8
|
+
- 多进程prometheus指标
|
|
9
|
+
- otel
|
|
10
|
+
|
|
11
|
+
## 分支规范
|
|
12
|
+
- master分支: 归档分支
|
|
13
|
+
- release/release-×.×.×: 发布分支,×.×.×为版本号;修改[pyproject.toml](pyproject.toml)中的version构建并发布包
|
|
14
|
+
- dev分支: 开发分支;合并至release分支时,需要提mr,mr需要填写版本号和功能描述;需要充分自测和review之后才能合并至release
|
|
15
|
+
- feat/feat-×××××: 功能分支;合并至dev时需要mr,mr需要填写功能名称和功能描述;新功能或较大重构需要建feat分支,较小的改动不必开feat分支
|
|
16
|
+
|
|
17
|
+
## 本地调试
|
|
18
|
+
|
|
19
|
+
本地开发需要打包验证,则先构建包(但不要发布),然后在本地目录离线安装
|
|
20
|
+
```shell
|
|
21
|
+
# 构建包
|
|
22
|
+
uv build
|
|
23
|
+
# 拷贝wheel包到本地测试目录下执行以下命令
|
|
24
|
+
uv add algo_backend_framework-*.*.*-py3-none-any.whl
|
|
25
|
+
# 调试时发现包有问题,只需要remove后重新打包,并重新add
|
|
26
|
+
uv remove algo-backend-framework
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 开发约定
|
|
30
|
+
- 代码提交前进行sonar扫描和语法检查,并进行代码格式化
|
|
31
|
+
```
|
|
32
|
+
uv run ruff format algo_backend/×××××
|
|
33
|
+
```
|
|
34
|
+
- 子模块__init__.py文件需要声明包内暴露模块,不建议用户通过包内文件名导入,以减少后期微调重构引起的不兼容
|
|
35
|
+
- 跨模块导入一律使用`from algo_backend.× import 模块名`的全限定包名,同级目录下导入从使用`from . import `
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## 发布
|
|
39
|
+
- 在release分支进行构建和推送
|
|
40
|
+
```shell
|
|
41
|
+
uv build
|
|
42
|
+
# 推送ctcdn nexus
|
|
43
|
+
uv publish --index ctcdn-pypi-upload
|
|
44
|
+
# 推送pypi
|
|
45
|
+
uv publish
|
|
46
|
+
```
|
|
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)
|