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.
Files changed (53) hide show
  1. algo_backend_framework-0.0.1/PKG-INFO +60 -0
  2. algo_backend_framework-0.0.1/README.md +46 -0
  3. algo_backend_framework-0.0.1/algo_backend/__init__.py +0 -0
  4. algo_backend_framework-0.0.1/algo_backend/config/__init__.py +8 -0
  5. algo_backend_framework-0.0.1/algo_backend/config/basic_config.py +13 -0
  6. algo_backend_framework-0.0.1/algo_backend/config/loguru_config.py +19 -0
  7. algo_backend_framework-0.0.1/algo_backend/exception/__init__.py +22 -0
  8. algo_backend_framework-0.0.1/algo_backend/exception/error_code_manage.py +126 -0
  9. algo_backend_framework-0.0.1/algo_backend/exception/exception.py +42 -0
  10. algo_backend_framework-0.0.1/algo_backend/exception/status_code.py +103 -0
  11. algo_backend_framework-0.0.1/algo_backend/handler/__init__.py +3 -0
  12. algo_backend_framework-0.0.1/algo_backend/handler/exception_to_vo.py +37 -0
  13. algo_backend_framework-0.0.1/algo_backend/handler/operation_handler.py +71 -0
  14. algo_backend_framework-0.0.1/algo_backend/intercept/__init__.py +9 -0
  15. algo_backend_framework-0.0.1/algo_backend/intercept/common.py +45 -0
  16. algo_backend_framework-0.0.1/algo_backend/intercept/http.py +40 -0
  17. algo_backend_framework-0.0.1/algo_backend/intercept/validate.py +78 -0
  18. algo_backend_framework-0.0.1/algo_backend/log/__init__.py +1 -0
  19. algo_backend_framework-0.0.1/algo_backend/log/common.py +16 -0
  20. algo_backend_framework-0.0.1/algo_backend/log/loguru/__init__.py +5 -0
  21. algo_backend_framework-0.0.1/algo_backend/log/loguru/log_clean.py +140 -0
  22. algo_backend_framework-0.0.1/algo_backend/log/loguru/log_setup.py +89 -0
  23. algo_backend_framework-0.0.1/algo_backend/log/loguru/log_starter.py +65 -0
  24. algo_backend_framework-0.0.1/algo_backend/log/loguru/patch_logging.py +83 -0
  25. algo_backend_framework-0.0.1/algo_backend/log/nblog/__init__.py +0 -0
  26. algo_backend_framework-0.0.1/algo_backend/metrics/__init__.py +22 -0
  27. algo_backend_framework-0.0.1/algo_backend/metrics/collector/__init__.py +12 -0
  28. algo_backend_framework-0.0.1/algo_backend/metrics/collector/common.py +17 -0
  29. algo_backend_framework-0.0.1/algo_backend/metrics/collector/gc_metrics.py +74 -0
  30. algo_backend_framework-0.0.1/algo_backend/metrics/collector/schedule_monitor.py +50 -0
  31. algo_backend_framework-0.0.1/algo_backend/metrics/collector/system_metrics.py +169 -0
  32. algo_backend_framework-0.0.1/algo_backend/metrics/http_metrics.py +56 -0
  33. algo_backend_framework-0.0.1/algo_backend/metrics/prometheus_context.py +55 -0
  34. algo_backend_framework-0.0.1/algo_backend/metrics/time_cost_metrics.py +146 -0
  35. algo_backend_framework-0.0.1/algo_backend/middleware/__init__.py +4 -0
  36. algo_backend_framework-0.0.1/algo_backend/middleware/cors.py +10 -0
  37. algo_backend_framework-0.0.1/algo_backend/middleware/metrics.py +12 -0
  38. algo_backend_framework-0.0.1/algo_backend/schema/__init__.py +3 -0
  39. algo_backend_framework-0.0.1/algo_backend/schema/vo.py +83 -0
  40. algo_backend_framework-0.0.1/algo_backend/starter/__init__.py +4 -0
  41. algo_backend_framework-0.0.1/algo_backend/starter/default_app_generator.py +169 -0
  42. algo_backend_framework-0.0.1/algo_backend/starter/default_service_starter.py +70 -0
  43. algo_backend_framework-0.0.1/algo_backend/starter/event_list.py +32 -0
  44. algo_backend_framework-0.0.1/algo_backend/utils/__init__.py +8 -0
  45. algo_backend_framework-0.0.1/algo_backend/utils/meta_class.py +50 -0
  46. algo_backend_framework-0.0.1/algo_backend/utils/utils.py +22 -0
  47. algo_backend_framework-0.0.1/algo_backend_framework.egg-info/PKG-INFO +60 -0
  48. algo_backend_framework-0.0.1/algo_backend_framework.egg-info/SOURCES.txt +51 -0
  49. algo_backend_framework-0.0.1/algo_backend_framework.egg-info/dependency_links.txt +1 -0
  50. algo_backend_framework-0.0.1/algo_backend_framework.egg-info/requires.txt +7 -0
  51. algo_backend_framework-0.0.1/algo_backend_framework.egg-info/top_level.txt +1 -0
  52. algo_backend_framework-0.0.1/pyproject.toml +28 -0
  53. 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,8 @@
1
+ from dotenv import load_dotenv
2
+
3
+ load_dotenv(".env")
4
+
5
+ from .basic_config import ErrorCodeConfig, ServiceConfig
6
+ from .loguru_config import LoguruConfig
7
+
8
+ __all__ = ["LoguruConfig", "ServiceConfig", "ErrorCodeConfig"]
@@ -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,3 @@
1
+ from .operation_handler import timing_and_exception_handler
2
+
3
+ __all__ = ["timing_and_exception_handler"]
@@ -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,9 @@
1
+ from .common import BasicExceptionInterceptor
2
+ from .http import HTTPExceptionInterceptor
3
+ from .validate import ValidateExceptionInterceptor
4
+
5
+ __all__ = [
6
+ "HTTPExceptionInterceptor",
7
+ "ValidateExceptionInterceptor",
8
+ "BasicExceptionInterceptor",
9
+ ]
@@ -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)