infomankit 0.3.23__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.
- infoman/__init__.py +1 -0
- infoman/cli/README.md +378 -0
- infoman/cli/__init__.py +7 -0
- infoman/cli/commands/__init__.py +3 -0
- infoman/cli/commands/init.py +312 -0
- infoman/cli/scaffold.py +634 -0
- infoman/cli/templates/Makefile.template +132 -0
- infoman/cli/templates/app/__init__.py.template +3 -0
- infoman/cli/templates/app/app.py.template +4 -0
- infoman/cli/templates/app/models_base.py.template +18 -0
- infoman/cli/templates/app/models_entity_init.py.template +11 -0
- infoman/cli/templates/app/models_schemas_init.py.template +11 -0
- infoman/cli/templates/app/repository_init.py.template +11 -0
- infoman/cli/templates/app/routers_init.py.template +15 -0
- infoman/cli/templates/app/services_init.py.template +11 -0
- infoman/cli/templates/app/static_index.html.template +39 -0
- infoman/cli/templates/app/static_main.js.template +31 -0
- infoman/cli/templates/app/static_style.css.template +111 -0
- infoman/cli/templates/app/utils_init.py.template +11 -0
- infoman/cli/templates/config/.env.dev.template +43 -0
- infoman/cli/templates/config/.env.prod.template +43 -0
- infoman/cli/templates/config/README.md.template +28 -0
- infoman/cli/templates/docker/.dockerignore.template +60 -0
- infoman/cli/templates/docker/Dockerfile.template +47 -0
- infoman/cli/templates/docker/README.md.template +240 -0
- infoman/cli/templates/docker/docker-compose.yml.template +81 -0
- infoman/cli/templates/docker/mysql_custom.cnf.template +42 -0
- infoman/cli/templates/docker/mysql_init.sql.template +15 -0
- infoman/cli/templates/project/.env.example.template +1 -0
- infoman/cli/templates/project/.gitignore.template +60 -0
- infoman/cli/templates/project/Makefile.template +38 -0
- infoman/cli/templates/project/README.md.template +137 -0
- infoman/cli/templates/project/deploy.sh.template +97 -0
- infoman/cli/templates/project/main.py.template +10 -0
- infoman/cli/templates/project/manage.sh.template +97 -0
- infoman/cli/templates/project/pyproject.toml.template +47 -0
- infoman/cli/templates/project/service.sh.template +203 -0
- infoman/config/__init__.py +25 -0
- infoman/config/base.py +67 -0
- infoman/config/db_cache.py +237 -0
- infoman/config/db_relation.py +181 -0
- infoman/config/db_vector.py +39 -0
- infoman/config/jwt.py +16 -0
- infoman/config/llm.py +16 -0
- infoman/config/log.py +627 -0
- infoman/config/mq.py +26 -0
- infoman/config/settings.py +65 -0
- infoman/llm/__init__.py +0 -0
- infoman/llm/llm.py +297 -0
- infoman/logger/__init__.py +57 -0
- infoman/logger/context.py +191 -0
- infoman/logger/core.py +358 -0
- infoman/logger/filters.py +157 -0
- infoman/logger/formatters.py +138 -0
- infoman/logger/handlers.py +276 -0
- infoman/logger/metrics.py +160 -0
- infoman/performance/README.md +583 -0
- infoman/performance/__init__.py +19 -0
- infoman/performance/cli.py +215 -0
- infoman/performance/config.py +166 -0
- infoman/performance/reporter.py +519 -0
- infoman/performance/runner.py +303 -0
- infoman/performance/standards.py +222 -0
- infoman/service/__init__.py +8 -0
- infoman/service/app.py +67 -0
- infoman/service/core/__init__.py +0 -0
- infoman/service/core/auth.py +105 -0
- infoman/service/core/lifespan.py +132 -0
- infoman/service/core/monitor.py +57 -0
- infoman/service/core/response.py +37 -0
- infoman/service/exception/__init__.py +7 -0
- infoman/service/exception/error.py +274 -0
- infoman/service/exception/exception.py +25 -0
- infoman/service/exception/handler.py +238 -0
- infoman/service/infrastructure/__init__.py +8 -0
- infoman/service/infrastructure/base.py +212 -0
- infoman/service/infrastructure/db_cache/__init__.py +8 -0
- infoman/service/infrastructure/db_cache/manager.py +194 -0
- infoman/service/infrastructure/db_relation/__init__.py +41 -0
- infoman/service/infrastructure/db_relation/manager.py +300 -0
- infoman/service/infrastructure/db_relation/manager_pro.py +408 -0
- infoman/service/infrastructure/db_relation/mysql.py +52 -0
- infoman/service/infrastructure/db_relation/pgsql.py +54 -0
- infoman/service/infrastructure/db_relation/sqllite.py +25 -0
- infoman/service/infrastructure/db_vector/__init__.py +40 -0
- infoman/service/infrastructure/db_vector/manager.py +201 -0
- infoman/service/infrastructure/db_vector/qdrant.py +322 -0
- infoman/service/infrastructure/mq/__init__.py +15 -0
- infoman/service/infrastructure/mq/manager.py +178 -0
- infoman/service/infrastructure/mq/nats/__init__.py +0 -0
- infoman/service/infrastructure/mq/nats/nats_client.py +57 -0
- infoman/service/infrastructure/mq/nats/nats_event_router.py +25 -0
- infoman/service/launch.py +284 -0
- infoman/service/middleware/__init__.py +7 -0
- infoman/service/middleware/base.py +41 -0
- infoman/service/middleware/logging.py +51 -0
- infoman/service/middleware/rate_limit.py +301 -0
- infoman/service/middleware/request_id.py +21 -0
- infoman/service/middleware/white_list.py +24 -0
- infoman/service/models/__init__.py +8 -0
- infoman/service/models/base.py +441 -0
- infoman/service/models/type/embed.py +70 -0
- infoman/service/routers/__init__.py +18 -0
- infoman/service/routers/health_router.py +311 -0
- infoman/service/routers/monitor_router.py +44 -0
- infoman/service/utils/__init__.py +8 -0
- infoman/service/utils/cache/__init__.py +0 -0
- infoman/service/utils/cache/cache.py +192 -0
- infoman/service/utils/module_loader.py +10 -0
- infoman/service/utils/parse.py +10 -0
- infoman/service/utils/resolver/__init__.py +8 -0
- infoman/service/utils/resolver/base.py +47 -0
- infoman/service/utils/resolver/resp.py +102 -0
- infoman/service/vector/__init__.py +20 -0
- infoman/service/vector/base.py +56 -0
- infoman/service/vector/qdrant.py +125 -0
- infoman/service/vector/service.py +67 -0
- infoman/utils/__init__.py +2 -0
- infoman/utils/decorators/__init__.py +8 -0
- infoman/utils/decorators/cache.py +137 -0
- infoman/utils/decorators/retry.py +99 -0
- infoman/utils/decorators/safe_execute.py +99 -0
- infoman/utils/decorators/timing.py +99 -0
- infoman/utils/encryption/__init__.py +8 -0
- infoman/utils/encryption/aes.py +66 -0
- infoman/utils/encryption/ecc.py +108 -0
- infoman/utils/encryption/rsa.py +112 -0
- infoman/utils/file/__init__.py +0 -0
- infoman/utils/file/handler.py +22 -0
- infoman/utils/hash/__init__.py +0 -0
- infoman/utils/hash/hash.py +61 -0
- infoman/utils/http/__init__.py +8 -0
- infoman/utils/http/client.py +62 -0
- infoman/utils/http/info.py +94 -0
- infoman/utils/http/result.py +19 -0
- infoman/utils/notification/__init__.py +8 -0
- infoman/utils/notification/feishu.py +35 -0
- infoman/utils/text/__init__.py +8 -0
- infoman/utils/text/extractor.py +111 -0
- infomankit-0.3.23.dist-info/METADATA +632 -0
- infomankit-0.3.23.dist-info/RECORD +143 -0
- infomankit-0.3.23.dist-info/WHEEL +4 -0
- infomankit-0.3.23.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
性能测试运行器
|
|
3
|
+
|
|
4
|
+
执行性能测试并收集结果
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import time
|
|
9
|
+
import statistics
|
|
10
|
+
from typing import List, Dict, Any
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
import httpx
|
|
14
|
+
from loguru import logger
|
|
15
|
+
|
|
16
|
+
from .config import TestConfig, APITestCase
|
|
17
|
+
from .standards import PerformanceStandards, StandardLevel
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class TestResult:
|
|
22
|
+
"""单次测试结果"""
|
|
23
|
+
test_case_name: str
|
|
24
|
+
url: str
|
|
25
|
+
method: str
|
|
26
|
+
status_code: int
|
|
27
|
+
response_time: float # 毫秒
|
|
28
|
+
success: bool
|
|
29
|
+
error_message: str = ""
|
|
30
|
+
timestamp: float = field(default_factory=time.time)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class AggregatedResult:
|
|
35
|
+
"""聚合测试结果"""
|
|
36
|
+
test_case_name: str
|
|
37
|
+
url: str
|
|
38
|
+
method: str
|
|
39
|
+
interface_type: str
|
|
40
|
+
|
|
41
|
+
# 请求统计
|
|
42
|
+
total_requests: int = 0
|
|
43
|
+
successful_requests: int = 0
|
|
44
|
+
failed_requests: int = 0
|
|
45
|
+
success_rate: float = 0.0
|
|
46
|
+
|
|
47
|
+
# 响应时间统计 (毫秒)
|
|
48
|
+
min_response_time: float = 0.0
|
|
49
|
+
max_response_time: float = 0.0
|
|
50
|
+
avg_response_time: float = 0.0
|
|
51
|
+
median_response_time: float = 0.0
|
|
52
|
+
p50_response_time: float = 0.0
|
|
53
|
+
p95_response_time: float = 0.0
|
|
54
|
+
p99_response_time: float = 0.0
|
|
55
|
+
|
|
56
|
+
# 吞吐量 (requests/second)
|
|
57
|
+
throughput: float = 0.0
|
|
58
|
+
|
|
59
|
+
# 性能评级
|
|
60
|
+
response_time_level: StandardLevel = StandardLevel.ACCEPTABLE
|
|
61
|
+
throughput_level: StandardLevel = StandardLevel.ACCEPTABLE
|
|
62
|
+
success_rate_level: StandardLevel = StandardLevel.ACCEPTABLE
|
|
63
|
+
overall_level: StandardLevel = StandardLevel.ACCEPTABLE
|
|
64
|
+
|
|
65
|
+
# 错误信息
|
|
66
|
+
error_messages: List[str] = field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PerformanceTestRunner:
|
|
70
|
+
"""性能测试运行器"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, config: TestConfig):
|
|
73
|
+
self.config = config
|
|
74
|
+
self.results: Dict[str, List[TestResult]] = {}
|
|
75
|
+
self.start_time: float = 0
|
|
76
|
+
self.end_time: float = 0
|
|
77
|
+
|
|
78
|
+
async def run(self) -> Dict[str, AggregatedResult]:
|
|
79
|
+
"""
|
|
80
|
+
运行性能测试
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
聚合结果字典 {test_case_name: AggregatedResult}
|
|
84
|
+
"""
|
|
85
|
+
logger.info(f"🚀 开始性能测试: {self.config.project_name}")
|
|
86
|
+
logger.info(f" 并发用户: {self.config.concurrent_users}")
|
|
87
|
+
logger.info(f" 持续时间: {self.config.duration}秒")
|
|
88
|
+
logger.info(f" 测试用例: {len(self.config.get_enabled_test_cases())}个")
|
|
89
|
+
|
|
90
|
+
self.start_time = time.time()
|
|
91
|
+
|
|
92
|
+
# 创建并发任务
|
|
93
|
+
tasks = []
|
|
94
|
+
for i in range(self.config.concurrent_users):
|
|
95
|
+
task = asyncio.create_task(self._user_task(i))
|
|
96
|
+
tasks.append(task)
|
|
97
|
+
# 控制启动速率
|
|
98
|
+
await asyncio.sleep(1 / self.config.spawn_rate)
|
|
99
|
+
|
|
100
|
+
# 等待所有任务完成
|
|
101
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
102
|
+
|
|
103
|
+
self.end_time = time.time()
|
|
104
|
+
|
|
105
|
+
logger.success(f"✅ 性能测试完成,耗时: {self.end_time - self.start_time:.2f}秒")
|
|
106
|
+
|
|
107
|
+
# 聚合结果
|
|
108
|
+
aggregated = self._aggregate_results()
|
|
109
|
+
|
|
110
|
+
return aggregated
|
|
111
|
+
|
|
112
|
+
async def _user_task(self, user_id: int):
|
|
113
|
+
"""单个用户的测试任务"""
|
|
114
|
+
test_cases = self.config.get_enabled_test_cases()
|
|
115
|
+
if not test_cases:
|
|
116
|
+
logger.warning("没有启用的测试用例")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
end_time = self.start_time + self.config.duration
|
|
120
|
+
|
|
121
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
122
|
+
while time.time() < end_time:
|
|
123
|
+
# 依次执行所有测试用例
|
|
124
|
+
for test_case in test_cases:
|
|
125
|
+
if time.time() >= end_time:
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
result = await self._execute_test_case(client, test_case)
|
|
129
|
+
|
|
130
|
+
# 保存结果
|
|
131
|
+
if test_case.name not in self.results:
|
|
132
|
+
self.results[test_case.name] = []
|
|
133
|
+
self.results[test_case.name].append(result)
|
|
134
|
+
|
|
135
|
+
# 思考时间
|
|
136
|
+
think_time = (
|
|
137
|
+
self.config.think_time_min +
|
|
138
|
+
(self.config.think_time_max - self.config.think_time_min) * 0.5
|
|
139
|
+
)
|
|
140
|
+
await asyncio.sleep(think_time)
|
|
141
|
+
|
|
142
|
+
async def _execute_test_case(
|
|
143
|
+
self,
|
|
144
|
+
client: httpx.AsyncClient,
|
|
145
|
+
test_case: APITestCase
|
|
146
|
+
) -> TestResult:
|
|
147
|
+
"""执行单个测试用例"""
|
|
148
|
+
url = self._build_url(test_case.url)
|
|
149
|
+
|
|
150
|
+
# 构建请求头
|
|
151
|
+
headers = {**self.config.global_headers, **test_case.headers}
|
|
152
|
+
|
|
153
|
+
# 添加认证
|
|
154
|
+
if self.config.auth_type == "bearer" and self.config.auth_token:
|
|
155
|
+
headers["Authorization"] = f"Bearer {self.config.auth_token}"
|
|
156
|
+
|
|
157
|
+
start_time = time.time()
|
|
158
|
+
success = False
|
|
159
|
+
status_code = 0
|
|
160
|
+
error_message = ""
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
response = await client.request(
|
|
164
|
+
method=test_case.method,
|
|
165
|
+
url=url,
|
|
166
|
+
headers=headers,
|
|
167
|
+
params=test_case.params,
|
|
168
|
+
json=test_case.json,
|
|
169
|
+
data=test_case.data,
|
|
170
|
+
timeout=test_case.timeout,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
status_code = response.status_code
|
|
174
|
+
success = 200 <= status_code < 300
|
|
175
|
+
|
|
176
|
+
if not success:
|
|
177
|
+
error_message = f"HTTP {status_code}: {response.text[:200]}"
|
|
178
|
+
|
|
179
|
+
except httpx.TimeoutException:
|
|
180
|
+
error_message = "请求超时"
|
|
181
|
+
except httpx.ConnectError:
|
|
182
|
+
error_message = "连接失败"
|
|
183
|
+
except Exception as e:
|
|
184
|
+
error_message = str(e)
|
|
185
|
+
|
|
186
|
+
end_time = time.time()
|
|
187
|
+
response_time = (end_time - start_time) * 1000 # 转换为毫秒
|
|
188
|
+
|
|
189
|
+
return TestResult(
|
|
190
|
+
test_case_name=test_case.name,
|
|
191
|
+
url=url,
|
|
192
|
+
method=test_case.method,
|
|
193
|
+
status_code=status_code,
|
|
194
|
+
response_time=response_time,
|
|
195
|
+
success=success,
|
|
196
|
+
error_message=error_message,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def _build_url(self, path: str) -> str:
|
|
200
|
+
"""构建完整 URL"""
|
|
201
|
+
if path.startswith("http://") or path.startswith("https://"):
|
|
202
|
+
return path
|
|
203
|
+
|
|
204
|
+
base_url = self.config.base_url.rstrip("/")
|
|
205
|
+
path = path.lstrip("/")
|
|
206
|
+
return f"{base_url}/{path}"
|
|
207
|
+
|
|
208
|
+
def _aggregate_results(self) -> Dict[str, AggregatedResult]:
|
|
209
|
+
"""聚合测试结果"""
|
|
210
|
+
aggregated = {}
|
|
211
|
+
test_duration = self.end_time - self.start_time
|
|
212
|
+
|
|
213
|
+
for test_case_name, results in self.results.items():
|
|
214
|
+
if not results:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
# 找到对应的测试用例配置
|
|
218
|
+
test_case = next(
|
|
219
|
+
(tc for tc in self.config.test_cases if tc.name == test_case_name),
|
|
220
|
+
None
|
|
221
|
+
)
|
|
222
|
+
interface_type = test_case.interface_type if test_case else "normal"
|
|
223
|
+
|
|
224
|
+
# 基本统计
|
|
225
|
+
total = len(results)
|
|
226
|
+
successful = sum(1 for r in results if r.success)
|
|
227
|
+
failed = total - successful
|
|
228
|
+
success_rate = (successful / total * 100) if total > 0 else 0
|
|
229
|
+
|
|
230
|
+
# 响应时间统计
|
|
231
|
+
response_times = [r.response_time for r in results]
|
|
232
|
+
response_times.sort()
|
|
233
|
+
|
|
234
|
+
min_rt = min(response_times) if response_times else 0
|
|
235
|
+
max_rt = max(response_times) if response_times else 0
|
|
236
|
+
avg_rt = statistics.mean(response_times) if response_times else 0
|
|
237
|
+
median_rt = statistics.median(response_times) if response_times else 0
|
|
238
|
+
|
|
239
|
+
# 百分位
|
|
240
|
+
p50 = self._percentile(response_times, 0.50)
|
|
241
|
+
p95 = self._percentile(response_times, 0.95)
|
|
242
|
+
p99 = self._percentile(response_times, 0.99)
|
|
243
|
+
|
|
244
|
+
# 吞吐量
|
|
245
|
+
throughput = total / test_duration if test_duration > 0 else 0
|
|
246
|
+
|
|
247
|
+
# 性能评级
|
|
248
|
+
rt_level = PerformanceStandards.evaluate_response_time(
|
|
249
|
+
avg_rt, interface_type
|
|
250
|
+
)
|
|
251
|
+
tp_level = PerformanceStandards.evaluate_throughput(
|
|
252
|
+
throughput, interface_type
|
|
253
|
+
)
|
|
254
|
+
sr_level = PerformanceStandards.evaluate_success_rate(success_rate)
|
|
255
|
+
|
|
256
|
+
# 综合评级 (取最差的)
|
|
257
|
+
overall_level = max(
|
|
258
|
+
[rt_level, tp_level, sr_level],
|
|
259
|
+
key=lambda x: list(StandardLevel).index(x)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# 错误信息
|
|
263
|
+
error_messages = [
|
|
264
|
+
r.error_message
|
|
265
|
+
for r in results
|
|
266
|
+
if not r.success and r.error_message
|
|
267
|
+
]
|
|
268
|
+
unique_errors = list(set(error_messages))[:10] # 最多10条
|
|
269
|
+
|
|
270
|
+
aggregated[test_case_name] = AggregatedResult(
|
|
271
|
+
test_case_name=test_case_name,
|
|
272
|
+
url=results[0].url,
|
|
273
|
+
method=results[0].method,
|
|
274
|
+
interface_type=interface_type,
|
|
275
|
+
total_requests=total,
|
|
276
|
+
successful_requests=successful,
|
|
277
|
+
failed_requests=failed,
|
|
278
|
+
success_rate=success_rate,
|
|
279
|
+
min_response_time=min_rt,
|
|
280
|
+
max_response_time=max_rt,
|
|
281
|
+
avg_response_time=avg_rt,
|
|
282
|
+
median_response_time=median_rt,
|
|
283
|
+
p50_response_time=p50,
|
|
284
|
+
p95_response_time=p95,
|
|
285
|
+
p99_response_time=p99,
|
|
286
|
+
throughput=throughput,
|
|
287
|
+
response_time_level=rt_level,
|
|
288
|
+
throughput_level=tp_level,
|
|
289
|
+
success_rate_level=sr_level,
|
|
290
|
+
overall_level=overall_level,
|
|
291
|
+
error_messages=unique_errors,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return aggregated
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _percentile(data: List[float], percentile: float) -> float:
|
|
298
|
+
"""计算百分位数"""
|
|
299
|
+
if not data:
|
|
300
|
+
return 0
|
|
301
|
+
sorted_data = sorted(data)
|
|
302
|
+
index = int(len(sorted_data) * percentile)
|
|
303
|
+
return sorted_data[min(index, len(sorted_data) - 1)]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
性能测试标准定义
|
|
3
|
+
|
|
4
|
+
定义不同级别的性能标准,用于评估接口性能
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StandardLevel(str, Enum):
|
|
13
|
+
"""标准级别"""
|
|
14
|
+
EXCELLENT = "excellent" # 优秀
|
|
15
|
+
GOOD = "good" # 良好
|
|
16
|
+
ACCEPTABLE = "acceptable" # 可接受
|
|
17
|
+
POOR = "poor" # 较差
|
|
18
|
+
CRITICAL = "critical" # 严重
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class PerformanceThreshold:
|
|
23
|
+
"""性能阈值"""
|
|
24
|
+
excellent: float # 优秀阈值
|
|
25
|
+
good: float # 良好阈值
|
|
26
|
+
acceptable: float # 可接受阈值
|
|
27
|
+
poor: float # 较差阈值
|
|
28
|
+
# 超过 poor 即为 critical
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PerformanceStandards:
|
|
32
|
+
"""
|
|
33
|
+
性能测试标准
|
|
34
|
+
|
|
35
|
+
定义了不同类型接口的性能标准
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# 标准定义 (单位: 毫秒)
|
|
39
|
+
STANDARDS = {
|
|
40
|
+
# 快速接口 (如健康检查、静态资源)
|
|
41
|
+
"fast": PerformanceThreshold(
|
|
42
|
+
excellent=10, # < 10ms
|
|
43
|
+
good=30, # < 30ms
|
|
44
|
+
acceptable=50, # < 50ms
|
|
45
|
+
poor=100, # < 100ms
|
|
46
|
+
),
|
|
47
|
+
|
|
48
|
+
# 一般接口 (如简单查询、列表)
|
|
49
|
+
"normal": PerformanceThreshold(
|
|
50
|
+
excellent=50, # < 50ms
|
|
51
|
+
good=100, # < 100ms
|
|
52
|
+
acceptable=200, # < 200ms
|
|
53
|
+
poor=500, # < 500ms
|
|
54
|
+
),
|
|
55
|
+
|
|
56
|
+
# 复杂接口 (如复杂查询、聚合)
|
|
57
|
+
"complex": PerformanceThreshold(
|
|
58
|
+
excellent=100, # < 100ms
|
|
59
|
+
good=200, # < 200ms
|
|
60
|
+
acceptable=500, # < 500ms
|
|
61
|
+
poor=1000, # < 1s
|
|
62
|
+
),
|
|
63
|
+
|
|
64
|
+
# 重型接口 (如文件处理、批量操作)
|
|
65
|
+
"heavy": PerformanceThreshold(
|
|
66
|
+
excellent=200, # < 200ms
|
|
67
|
+
good=500, # < 500ms
|
|
68
|
+
acceptable=1000, # < 1s
|
|
69
|
+
poor=3000, # < 3s
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# 吞吐量标准 (requests/second)
|
|
74
|
+
THROUGHPUT_STANDARDS = {
|
|
75
|
+
"fast": {
|
|
76
|
+
"excellent": 1000,
|
|
77
|
+
"good": 500,
|
|
78
|
+
"acceptable": 200,
|
|
79
|
+
"poor": 100,
|
|
80
|
+
},
|
|
81
|
+
"normal": {
|
|
82
|
+
"excellent": 500,
|
|
83
|
+
"good": 200,
|
|
84
|
+
"acceptable": 100,
|
|
85
|
+
"poor": 50,
|
|
86
|
+
},
|
|
87
|
+
"complex": {
|
|
88
|
+
"excellent": 200,
|
|
89
|
+
"good": 100,
|
|
90
|
+
"acceptable": 50,
|
|
91
|
+
"poor": 20,
|
|
92
|
+
},
|
|
93
|
+
"heavy": {
|
|
94
|
+
"excellent": 100,
|
|
95
|
+
"good": 50,
|
|
96
|
+
"acceptable": 20,
|
|
97
|
+
"poor": 10,
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# 并发用户标准
|
|
102
|
+
CONCURRENCY_STANDARDS = {
|
|
103
|
+
"low": 10, # 低并发
|
|
104
|
+
"medium": 50, # 中并发
|
|
105
|
+
"high": 100, # 高并发
|
|
106
|
+
"extreme": 500, # 极限并发
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# 成功率标准
|
|
110
|
+
SUCCESS_RATE_STANDARDS = {
|
|
111
|
+
"excellent": 99.9, # 99.9%
|
|
112
|
+
"good": 99.0, # 99%
|
|
113
|
+
"acceptable": 95.0, # 95%
|
|
114
|
+
"poor": 90.0, # 90%
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def evaluate_response_time(
|
|
119
|
+
cls,
|
|
120
|
+
response_time: float,
|
|
121
|
+
interface_type: str = "normal"
|
|
122
|
+
) -> StandardLevel:
|
|
123
|
+
"""
|
|
124
|
+
评估响应时间
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
response_time: 响应时间 (毫秒)
|
|
128
|
+
interface_type: 接口类型 (fast/normal/complex/heavy)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
标准级别
|
|
132
|
+
"""
|
|
133
|
+
threshold = cls.STANDARDS.get(interface_type, cls.STANDARDS["normal"])
|
|
134
|
+
|
|
135
|
+
if response_time <= threshold.excellent:
|
|
136
|
+
return StandardLevel.EXCELLENT
|
|
137
|
+
elif response_time <= threshold.good:
|
|
138
|
+
return StandardLevel.GOOD
|
|
139
|
+
elif response_time <= threshold.acceptable:
|
|
140
|
+
return StandardLevel.ACCEPTABLE
|
|
141
|
+
elif response_time <= threshold.poor:
|
|
142
|
+
return StandardLevel.POOR
|
|
143
|
+
else:
|
|
144
|
+
return StandardLevel.CRITICAL
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def evaluate_throughput(
|
|
148
|
+
cls,
|
|
149
|
+
throughput: float,
|
|
150
|
+
interface_type: str = "normal"
|
|
151
|
+
) -> StandardLevel:
|
|
152
|
+
"""评估吞吐量"""
|
|
153
|
+
standards = cls.THROUGHPUT_STANDARDS.get(
|
|
154
|
+
interface_type,
|
|
155
|
+
cls.THROUGHPUT_STANDARDS["normal"]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if throughput >= standards["excellent"]:
|
|
159
|
+
return StandardLevel.EXCELLENT
|
|
160
|
+
elif throughput >= standards["good"]:
|
|
161
|
+
return StandardLevel.GOOD
|
|
162
|
+
elif throughput >= standards["acceptable"]:
|
|
163
|
+
return StandardLevel.ACCEPTABLE
|
|
164
|
+
elif throughput >= standards["poor"]:
|
|
165
|
+
return StandardLevel.POOR
|
|
166
|
+
else:
|
|
167
|
+
return StandardLevel.CRITICAL
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def evaluate_success_rate(cls, success_rate: float) -> StandardLevel:
|
|
171
|
+
"""评估成功率"""
|
|
172
|
+
if success_rate >= cls.SUCCESS_RATE_STANDARDS["excellent"]:
|
|
173
|
+
return StandardLevel.EXCELLENT
|
|
174
|
+
elif success_rate >= cls.SUCCESS_RATE_STANDARDS["good"]:
|
|
175
|
+
return StandardLevel.GOOD
|
|
176
|
+
elif success_rate >= cls.SUCCESS_RATE_STANDARDS["acceptable"]:
|
|
177
|
+
return StandardLevel.ACCEPTABLE
|
|
178
|
+
elif success_rate >= cls.SUCCESS_RATE_STANDARDS["poor"]:
|
|
179
|
+
return StandardLevel.POOR
|
|
180
|
+
else:
|
|
181
|
+
return StandardLevel.CRITICAL
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def get_threshold(cls, interface_type: str = "normal") -> PerformanceThreshold:
|
|
185
|
+
"""获取性能阈值"""
|
|
186
|
+
return cls.STANDARDS.get(interface_type, cls.STANDARDS["normal"])
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def get_level_color(cls, level: StandardLevel) -> str:
|
|
190
|
+
"""获取级别对应的颜色"""
|
|
191
|
+
colors = {
|
|
192
|
+
StandardLevel.EXCELLENT: "#10b981", # 绿色
|
|
193
|
+
StandardLevel.GOOD: "#3b82f6", # 蓝色
|
|
194
|
+
StandardLevel.ACCEPTABLE: "#f59e0b", # 橙色
|
|
195
|
+
StandardLevel.POOR: "#ef4444", # 红色
|
|
196
|
+
StandardLevel.CRITICAL: "#991b1b", # 深红色
|
|
197
|
+
}
|
|
198
|
+
return colors.get(level, "#6b7280")
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def get_level_label(cls, level: StandardLevel) -> str:
|
|
202
|
+
"""获取级别标签 (中文)"""
|
|
203
|
+
labels = {
|
|
204
|
+
StandardLevel.EXCELLENT: "优秀",
|
|
205
|
+
StandardLevel.GOOD: "良好",
|
|
206
|
+
StandardLevel.ACCEPTABLE: "可接受",
|
|
207
|
+
StandardLevel.POOR: "较差",
|
|
208
|
+
StandardLevel.CRITICAL: "严重",
|
|
209
|
+
}
|
|
210
|
+
return labels.get(level, "未知")
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def get_recommendation(cls, level: StandardLevel) -> str:
|
|
214
|
+
"""获取优化建议"""
|
|
215
|
+
recommendations = {
|
|
216
|
+
StandardLevel.EXCELLENT: "性能优异,继续保持",
|
|
217
|
+
StandardLevel.GOOD: "性能良好,可考虑进一步优化",
|
|
218
|
+
StandardLevel.ACCEPTABLE: "性能可接受,建议优化以提升用户体验",
|
|
219
|
+
StandardLevel.POOR: "性能较差,建议优先优化此接口",
|
|
220
|
+
StandardLevel.CRITICAL: "性能严重不足,需要立即优化",
|
|
221
|
+
}
|
|
222
|
+
return recommendations.get(level, "")
|
infoman/service/app.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from infoman.config.settings import settings
|
|
5
|
+
from infoman.logger import setup_logger
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
8
|
+
from fastapi.middleware.gzip import GZipMiddleware
|
|
9
|
+
from infoman.service.middleware.logging import LoggingMiddleware
|
|
10
|
+
from infoman.service.middleware.request_id import RequestIDMiddleware
|
|
11
|
+
from infoman.service.routers import api_router
|
|
12
|
+
from infoman.config.settings import settings as config
|
|
13
|
+
from infoman.service.core.response import ProRJSONResponse
|
|
14
|
+
from infoman.service.core.monitor import instrumentator
|
|
15
|
+
from infoman.service.core.lifespan import lifespan
|
|
16
|
+
from infoman.service.exception.handler import register_exception_handlers
|
|
17
|
+
|
|
18
|
+
# ============ 日志配置 ============
|
|
19
|
+
setup_logger()
|
|
20
|
+
|
|
21
|
+
# ============ 创建 FastAPI 应用实例 ============
|
|
22
|
+
application = FastAPI(
|
|
23
|
+
title=config.APP_NAME,
|
|
24
|
+
docs_url=config.DOCS_URL,
|
|
25
|
+
redoc_url=config.APP_REDOC_URL,
|
|
26
|
+
description=config.APP_DESCRIPTION,
|
|
27
|
+
default_response_class=ProRJSONResponse,
|
|
28
|
+
lifespan=lifespan
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# ============ 异常处理器 ============
|
|
32
|
+
register_exception_handlers(application)
|
|
33
|
+
|
|
34
|
+
# ============ 中间件(添加顺序和执行顺序相反-洋葱模型) ============
|
|
35
|
+
application.add_middleware(GZipMiddleware, minimum_size=1000)
|
|
36
|
+
application.add_middleware(
|
|
37
|
+
CORSMiddleware,
|
|
38
|
+
allow_origins=config.ALLOW_ORIGINS,
|
|
39
|
+
allow_credentials=config.ALLOW_CREDENTIALS,
|
|
40
|
+
allow_methods=config.ALLOW_METHODS,
|
|
41
|
+
allow_headers=config.ALLOW_HEADERS,
|
|
42
|
+
max_age=config.MAX_AGE,
|
|
43
|
+
)
|
|
44
|
+
application.add_middleware(RequestIDMiddleware)
|
|
45
|
+
application.add_middleware(LoggingMiddleware)
|
|
46
|
+
|
|
47
|
+
# ============ 内置路由(可选,用户可以选择不使用) ============
|
|
48
|
+
if config.USE_DEFAULT_ROUTER:
|
|
49
|
+
application.include_router(api_router)
|
|
50
|
+
|
|
51
|
+
if config.USE_TEMPLATES:
|
|
52
|
+
from fastapi.templating import Jinja2Templates
|
|
53
|
+
application.state.templates = Jinja2Templates(directory=config.TEMPLATE_DIR)
|
|
54
|
+
|
|
55
|
+
if config.USE_STATIC:
|
|
56
|
+
from fastapi.staticfiles import StaticFiles
|
|
57
|
+
application.mount(
|
|
58
|
+
path=config.STATIC_URL,
|
|
59
|
+
app=StaticFiles(directory=config.STATIC_DIR),
|
|
60
|
+
name=config.STATIC_NAME
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# ============ Prometheus 监控 ============
|
|
64
|
+
if config.USE_PROMETHEUS_ROUTER:
|
|
65
|
+
instrumentator.instrument(application).expose(application, endpoint="/metrics")
|
|
66
|
+
|
|
67
|
+
|
|
File without changes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
# Time :2025/6/21 23:21
|
|
6
|
+
# Author :Maxwell
|
|
7
|
+
# Description:
|
|
8
|
+
"""
|
|
9
|
+
import jwt
|
|
10
|
+
from datetime import timedelta, datetime
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Optional, Dict, Any, TypeVar
|
|
13
|
+
|
|
14
|
+
from fastapi import Request, Depends
|
|
15
|
+
from fastapi.security.oauth2 import OAuth2PasswordBearer
|
|
16
|
+
from jwt import PyJWTError
|
|
17
|
+
from pydantic import ValidationError, BaseModel
|
|
18
|
+
|
|
19
|
+
from infoman.config import settings as config
|
|
20
|
+
from infoman.service.exception import exception
|
|
21
|
+
from infoman.service.exception import error
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SecurityConstants:
|
|
27
|
+
REDIS_USER_KEY_PREFIX = "U_API_AUTH_KEY_"
|
|
28
|
+
REDIS_OPEN_API_KEY_PREFIX = "O_API_AUTH_KEY_"
|
|
29
|
+
REDIS_CACHE_EXPIRE = 3 * 3600
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TokenPayload(BaseModel):
|
|
33
|
+
exp: Optional[datetime] = None
|
|
34
|
+
user_id: Optional[int] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TokenType(str, Enum):
|
|
38
|
+
ACCESS = "access"
|
|
39
|
+
REFRESH = "refresh"
|
|
40
|
+
OPEN_API = "open_api"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class JWTHandler:
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def create_token(
|
|
47
|
+
data: Dict[str, Any],
|
|
48
|
+
expires_delta: Optional[timedelta] = None,
|
|
49
|
+
token_type: TokenType = TokenType.ACCESS,
|
|
50
|
+
) -> str:
|
|
51
|
+
payload = data.copy()
|
|
52
|
+
if expires_delta:
|
|
53
|
+
expire = datetime.utcnow() + expires_delta
|
|
54
|
+
elif token_type == TokenType.ACCESS:
|
|
55
|
+
expire = datetime.utcnow() + timedelta(
|
|
56
|
+
minutes=config.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
expire = datetime.utcnow() + timedelta(days=365)
|
|
60
|
+
payload.update({"exp": expire, "token_type": token_type})
|
|
61
|
+
return jwt.encode(
|
|
62
|
+
payload, config.JWT_SECRET_KEY, algorithm=config.JWT_ALGORITHM
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def decode_token(token: str) -> Dict[str, Any]:
|
|
67
|
+
try:
|
|
68
|
+
payload = jwt.decode(
|
|
69
|
+
token, config.JWT_SECRET_KEY, algorithms=[config.JWT_ALGORITHM]
|
|
70
|
+
)
|
|
71
|
+
return payload
|
|
72
|
+
except jwt.ExpiredSignatureError:
|
|
73
|
+
raise exception.AppException(error.SecurityError.TOKEN_EXPIRED)
|
|
74
|
+
except (jwt.InvalidTokenError, PyJWTError, ValidationError):
|
|
75
|
+
raise exception.AppException(error.SecurityError.INVALID_CREDENTIALS)
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def verify_token(token: str) -> TokenPayload:
|
|
79
|
+
try:
|
|
80
|
+
payload = JWTHandler.decode_token(token)
|
|
81
|
+
token_data = TokenPayload(**payload)
|
|
82
|
+
if token_data.exp and token_data.exp < datetime.utcnow():
|
|
83
|
+
raise exception.AppException(error.SecurityError.TOKEN_EXPIRED)
|
|
84
|
+
return token_data
|
|
85
|
+
except ValidationError:
|
|
86
|
+
raise exception.AppException(error.SecurityError.INVALID_TOKEN)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TokenExtractor:
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def get_user_id(token: str) -> Optional[int]:
|
|
93
|
+
try:
|
|
94
|
+
payload = JWTHandler.decode_token(token)
|
|
95
|
+
return payload.get("user_id")
|
|
96
|
+
except Exception:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def get_open_user_id(token: str) -> Optional[int]:
|
|
101
|
+
try:
|
|
102
|
+
payload = JWTHandler.decode_token(token)
|
|
103
|
+
return payload.get("open_user_id")
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|