sycommon-python-lib 0.1.56b6__py3-none-any.whl → 0.1.56b7__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.
- sycommon/config/Config.py +16 -0
- sycommon/config/SentryConfig.py +13 -0
- sycommon/logging/kafka_log.py +185 -432
- sycommon/middleware/exception.py +10 -16
- sycommon/middleware/timeout.py +2 -1
- sycommon/middleware/traceid.py +67 -61
- sycommon/notice/uvicorn_monitor.py +32 -27
- sycommon/rabbitmq/rabbitmq_service.py +1 -1
- sycommon/sentry/__init__.py +0 -0
- sycommon/sentry/sy_sentry.py +34 -0
- sycommon/services.py +4 -0
- sycommon/synacos/nacos_service.py +101 -82
- {sycommon_python_lib-0.1.56b6.dist-info → sycommon_python_lib-0.1.56b7.dist-info}/METADATA +2 -1
- {sycommon_python_lib-0.1.56b6.dist-info → sycommon_python_lib-0.1.56b7.dist-info}/RECORD +17 -14
- {sycommon_python_lib-0.1.56b6.dist-info → sycommon_python_lib-0.1.56b7.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.1.56b6.dist-info → sycommon_python_lib-0.1.56b7.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.1.56b6.dist-info → sycommon_python_lib-0.1.56b7.dist-info}/top_level.txt +0 -0
sycommon/middleware/exception.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from fastapi import Request, HTTPException
|
|
2
2
|
from fastapi.responses import JSONResponse
|
|
3
3
|
from pydantic import ValidationError
|
|
4
|
-
import
|
|
4
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def setup_exception_handler(app, config: dict):
|
|
@@ -15,7 +15,7 @@ def setup_exception_handler(app, config: dict):
|
|
|
15
15
|
int_MaxBytes = int(MaxBytes) / 1024 / 1024
|
|
16
16
|
return JSONResponse(
|
|
17
17
|
content={
|
|
18
|
-
'code': 413, 'error': f'File size exceeds the allowed limit of {int_MaxBytes}MB.'},
|
|
18
|
+
'code': 413, 'error': f'File size exceeds the allowed limit of {int_MaxBytes}MB.', 'traceId': SYLogger.get_trace_id()},
|
|
19
19
|
status_code=413
|
|
20
20
|
)
|
|
21
21
|
|
|
@@ -27,7 +27,8 @@ def setup_exception_handler(app, config: dict):
|
|
|
27
27
|
content={
|
|
28
28
|
"code": exc.status_code,
|
|
29
29
|
"message": exc.detail,
|
|
30
|
-
"path": str(request.url.path)
|
|
30
|
+
"path": str(request.url.path),
|
|
31
|
+
"traceId": SYLogger.get_trace_id()
|
|
31
32
|
}
|
|
32
33
|
)
|
|
33
34
|
|
|
@@ -39,7 +40,8 @@ def setup_exception_handler(app, config: dict):
|
|
|
39
40
|
content={
|
|
40
41
|
"code": 400,
|
|
41
42
|
"message": "参数验证失败",
|
|
42
|
-
"details": exc.errors()
|
|
43
|
+
"details": exc.errors(),
|
|
44
|
+
"traceId": SYLogger.get_trace_id()
|
|
43
45
|
}
|
|
44
46
|
)
|
|
45
47
|
|
|
@@ -55,30 +57,22 @@ def setup_exception_handler(app, config: dict):
|
|
|
55
57
|
status_code=exc.code,
|
|
56
58
|
content={
|
|
57
59
|
"code": exc.code,
|
|
58
|
-
"message": exc.message
|
|
60
|
+
"message": exc.message,
|
|
61
|
+
"traceId": SYLogger.get_trace_id()
|
|
59
62
|
}
|
|
60
63
|
)
|
|
61
64
|
|
|
62
65
|
# 5. 全局异常处理器(捕获所有未处理的异常)
|
|
63
66
|
@app.exception_handler(Exception)
|
|
64
67
|
async def global_exception_handler(request: Request, exc: Exception):
|
|
65
|
-
# 记录详细错误信息
|
|
66
|
-
error_msg = f"请求路径: {request.url}\n"
|
|
67
|
-
error_msg += f"错误类型: {type(exc).__name__}\n"
|
|
68
|
-
error_msg += f"错误信息: {str(exc)}\n"
|
|
69
|
-
error_msg += f"堆栈信息: {traceback.format_exc()}"
|
|
70
|
-
|
|
71
|
-
# 使用你的日志服务记录错误
|
|
72
|
-
from sycommon.logging.kafka_log import SYLogger
|
|
73
|
-
SYLogger.error(error_msg)
|
|
74
|
-
|
|
75
68
|
# 返回统一格式的错误响应(生产环境可选择不返回详细信息)
|
|
76
69
|
return JSONResponse(
|
|
77
70
|
status_code=500,
|
|
78
71
|
content={
|
|
79
72
|
"code": 500,
|
|
80
73
|
"message": "服务器内部错误,请稍后重试",
|
|
81
|
-
"detail": str(exc) if config.get('DEBUG', False) else "Internal Server Error"
|
|
74
|
+
"detail": str(exc) if config.get('DEBUG', False) else "Internal Server Error",
|
|
75
|
+
"traceId": SYLogger.get_trace_id()
|
|
82
76
|
}
|
|
83
77
|
)
|
|
84
78
|
|
sycommon/middleware/timeout.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import time
|
|
3
3
|
from fastapi import Request
|
|
4
4
|
from fastapi.responses import JSONResponse
|
|
5
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def setup_request_timeout_middleware(app, config: dict):
|
|
@@ -14,6 +15,6 @@ def setup_request_timeout_middleware(app, config: dict):
|
|
|
14
15
|
response = await call_next(request)
|
|
15
16
|
duration = time.time() - request.state.start_time
|
|
16
17
|
if duration > REQUEST_TIMEOUT:
|
|
17
|
-
return JSONResponse(content={'code': 1, 'error': 'Request timed out'}, status_code=504)
|
|
18
|
+
return JSONResponse(content={'code': 1, 'error': 'Request timed out', 'traceId': SYLogger.get_trace_id()}, status_code=504)
|
|
18
19
|
return response
|
|
19
20
|
return app
|
sycommon/middleware/traceid.py
CHANGED
|
@@ -10,11 +10,9 @@ from sycommon.tools.snowflake import Snowflake
|
|
|
10
10
|
def setup_trace_id_handler(app):
|
|
11
11
|
@app.middleware("http")
|
|
12
12
|
async def trace_id_and_log_middleware(request: Request, call_next):
|
|
13
|
-
# ========== 1.
|
|
14
|
-
# 优先从请求头读取(兼容任意大小写)
|
|
13
|
+
# ========== 1. 请求阶段:获取/生成 TraceID ==========
|
|
15
14
|
trace_id = request.headers.get(
|
|
16
15
|
"x-traceId-header") or request.headers.get("x-traceid-header")
|
|
17
|
-
# 无则生成雪花ID
|
|
18
16
|
if not trace_id:
|
|
19
17
|
trace_id = Snowflake.id
|
|
20
18
|
|
|
@@ -37,38 +35,30 @@ def setup_trace_id_handler(app):
|
|
|
37
35
|
|
|
38
36
|
if is_json_content and request.method in ["POST", "PUT", "PATCH"]:
|
|
39
37
|
try:
|
|
40
|
-
# 兼容纯文本格式的 JSON
|
|
38
|
+
# 兼容纯文本格式的 JSON
|
|
41
39
|
if "text/plain" in content_type:
|
|
42
40
|
raw_text = await request.text(encoding="utf-8")
|
|
43
41
|
request_body = json.loads(raw_text)
|
|
44
42
|
else:
|
|
45
|
-
# application/json 直接解析
|
|
46
43
|
request_body = await request.json()
|
|
47
|
-
except Exception
|
|
44
|
+
except Exception:
|
|
48
45
|
try:
|
|
49
46
|
request_body = await request.json()
|
|
50
47
|
except Exception as e:
|
|
51
|
-
# 精准捕获 JSON 解析错误(而非泛 Exception)
|
|
52
48
|
request_body = {"error": f"JSON parse failed: {str(e)}"}
|
|
53
49
|
|
|
54
50
|
elif "multipart/form-data" in content_type and request.method in ["POST", "PUT"]:
|
|
55
51
|
try:
|
|
56
|
-
# 从请求头中提取boundary
|
|
57
52
|
boundary = None
|
|
58
53
|
if "boundary=" in content_type:
|
|
59
54
|
boundary = content_type.split("boundary=")[1].strip()
|
|
60
55
|
boundary = boundary.encode('ascii')
|
|
61
56
|
|
|
62
57
|
if boundary:
|
|
63
|
-
# 读取原始请求体
|
|
64
58
|
body = await request.body()
|
|
65
|
-
|
|
66
|
-
# 尝试从原始请求体中提取文件名
|
|
67
59
|
parts = body.split(boundary)
|
|
68
60
|
for part in parts:
|
|
69
61
|
part_str = part.decode('utf-8', errors='ignore')
|
|
70
|
-
|
|
71
|
-
# 使用正则表达式查找文件名
|
|
72
62
|
filename_match = re.search(
|
|
73
63
|
r'filename="([^"]+)"', part_str)
|
|
74
64
|
if filename_match:
|
|
@@ -82,31 +72,36 @@ def setup_trace_id_handler(app):
|
|
|
82
72
|
request_body = {
|
|
83
73
|
"error": f"Failed to process form data: {str(e)}"}
|
|
84
74
|
|
|
85
|
-
#
|
|
75
|
+
# 构建请求日志
|
|
86
76
|
request_message = {
|
|
87
|
-
"traceId": trace_id,
|
|
77
|
+
"traceId": trace_id,
|
|
88
78
|
"method": request.method,
|
|
89
79
|
"url": str(request.url),
|
|
90
80
|
"query_params": query_params,
|
|
91
81
|
"request_body": request_body,
|
|
92
82
|
"uploaded_files": files_info if files_info else None
|
|
93
83
|
}
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
SYLogger.info(json.dumps(request_message, ensure_ascii=False))
|
|
85
|
+
|
|
86
|
+
# 标记位:默认认为会发生异常
|
|
87
|
+
# 这样如果中途代码报错跳转到 except,finally 就不会 reset,保留 trace_id 给 Exception Handler
|
|
88
|
+
had_exception = True
|
|
96
89
|
|
|
97
90
|
try:
|
|
98
|
-
# 处理请求
|
|
91
|
+
# ========== 2. 处理请求 ==========
|
|
99
92
|
response = await call_next(request)
|
|
100
93
|
|
|
101
|
-
#
|
|
102
|
-
|
|
94
|
+
# ========== 3. 响应处理阶段 ==========
|
|
95
|
+
# 注意:此阶段发生的任何异常都会被下方的 except 捕获
|
|
96
|
+
# 从而保证 trace_id 不被清除,能够透传
|
|
103
97
|
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
response_content_type = response.headers.get(
|
|
99
|
+
"content-type", "").lower()
|
|
100
|
+
|
|
101
|
+
# 处理 SSE (Server-Sent Events)
|
|
102
|
+
if "text/event-stream" in response_content_type:
|
|
106
103
|
try:
|
|
107
|
-
# 强制写入 x-traceId-header 到响应头
|
|
108
104
|
response.headers["x-traceId-header"] = trace_id
|
|
109
|
-
# 确保前端能读取(仅补充暴露头,不覆盖原有值)
|
|
110
105
|
expose_headers = response.headers.get(
|
|
111
106
|
"access-control-expose-headers", "")
|
|
112
107
|
if expose_headers:
|
|
@@ -115,29 +110,31 @@ def setup_trace_id_handler(app):
|
|
|
115
110
|
"access-control-expose-headers"] = f"{expose_headers}, x-traceId-header"
|
|
116
111
|
else:
|
|
117
112
|
response.headers["access-control-expose-headers"] = "x-traceId-header"
|
|
118
|
-
|
|
113
|
+
|
|
114
|
+
# SSE 必须移除 Content-Length
|
|
119
115
|
headers_lower = {
|
|
120
116
|
k.lower(): k for k in response.headers.keys()}
|
|
121
117
|
if "content-length" in headers_lower:
|
|
122
118
|
del response.headers[headers_lower["content-length"]]
|
|
123
119
|
except AttributeError:
|
|
124
|
-
#
|
|
120
|
+
# 流式响应头只读处理
|
|
125
121
|
new_headers = dict(response.headers) if hasattr(
|
|
126
122
|
response.headers, 'items') else {}
|
|
127
|
-
new_headers["x-traceId-header"] = trace_id
|
|
128
|
-
# 保留原有暴露头,补充 traceId
|
|
123
|
+
new_headers["x-traceId-header"] = trace_id
|
|
129
124
|
if "access-control-expose-headers" in new_headers:
|
|
130
125
|
if "x-traceId-header" not in new_headers["access-control-expose-headers"].lower():
|
|
131
126
|
new_headers["access-control-expose-headers"] += ", x-traceId-header"
|
|
132
127
|
else:
|
|
133
128
|
new_headers["access-control-expose-headers"] = "x-traceId-header"
|
|
134
|
-
# 移除 Content-Length
|
|
135
129
|
new_headers.pop("content-length", None)
|
|
136
130
|
response.init_headers(new_headers)
|
|
131
|
+
|
|
132
|
+
# SSE 不处理 Body,直接返回
|
|
133
|
+
had_exception = False
|
|
137
134
|
return response
|
|
138
135
|
|
|
139
|
-
#
|
|
140
|
-
# 备份 CORS
|
|
136
|
+
# 处理非 SSE 响应
|
|
137
|
+
# 备份 CORS 头
|
|
141
138
|
cors_headers = {}
|
|
142
139
|
cors_header_keys = [
|
|
143
140
|
"access-control-allow-origin",
|
|
@@ -153,7 +150,7 @@ def setup_trace_id_handler(app):
|
|
|
153
150
|
cors_headers[key] = response.headers[k]
|
|
154
151
|
break
|
|
155
152
|
|
|
156
|
-
# 合并
|
|
153
|
+
# 合并 Headers
|
|
157
154
|
merged_headers = merge_headers(
|
|
158
155
|
source_headers=request.headers,
|
|
159
156
|
target_headers=response.headers,
|
|
@@ -161,10 +158,11 @@ def setup_trace_id_handler(app):
|
|
|
161
158
|
delete_keys={'content-length', 'accept', 'content-type'}
|
|
162
159
|
)
|
|
163
160
|
|
|
164
|
-
# 强制加入 x-traceId-header
|
|
161
|
+
# 强制加入 x-traceId-header
|
|
165
162
|
merged_headers["x-traceId-header"] = trace_id
|
|
166
|
-
# 恢复 CORS 头 + 补充 traceId 到暴露头
|
|
167
163
|
merged_headers.update(cors_headers)
|
|
164
|
+
|
|
165
|
+
# 更新暴露头
|
|
168
166
|
expose_headers = merged_headers.get(
|
|
169
167
|
"access-control-expose-headers", "")
|
|
170
168
|
if expose_headers:
|
|
@@ -173,7 +171,7 @@ def setup_trace_id_handler(app):
|
|
|
173
171
|
else:
|
|
174
172
|
merged_headers["access-control-expose-headers"] = "x-traceId-header"
|
|
175
173
|
|
|
176
|
-
#
|
|
174
|
+
# 应用 Headers
|
|
177
175
|
if hasattr(response.headers, 'clear'):
|
|
178
176
|
response.headers.clear()
|
|
179
177
|
for k, v in merged_headers.items():
|
|
@@ -187,27 +185,26 @@ def setup_trace_id_handler(app):
|
|
|
187
185
|
except (AttributeError, KeyError):
|
|
188
186
|
pass
|
|
189
187
|
|
|
190
|
-
#
|
|
188
|
+
# 处理响应体
|
|
191
189
|
response_body = b""
|
|
192
190
|
try:
|
|
193
191
|
async for chunk in response.body_iterator:
|
|
194
192
|
response_body += chunk
|
|
195
193
|
|
|
196
|
-
# 获取 Content-Disposition(统一小写)
|
|
197
194
|
content_disposition = response.headers.get(
|
|
198
195
|
"content-disposition", "").lower()
|
|
199
196
|
|
|
200
|
-
# JSON
|
|
201
|
-
if "application/json" in
|
|
197
|
+
# JSON 响应体注入 traceId
|
|
198
|
+
if "application/json" in response_content_type and not content_disposition.startswith("attachment"):
|
|
202
199
|
try:
|
|
203
200
|
data = json.loads(response_body)
|
|
204
201
|
new_body = response_body
|
|
205
|
-
if data:
|
|
206
|
-
data["traceId"] = trace_id
|
|
202
|
+
if isinstance(data, dict):
|
|
203
|
+
data["traceId"] = trace_id
|
|
207
204
|
new_body = json.dumps(
|
|
208
205
|
data, ensure_ascii=False).encode()
|
|
209
206
|
|
|
210
|
-
#
|
|
207
|
+
# 重建 Response 以更新 Body 和 Content-Length
|
|
211
208
|
response = Response(
|
|
212
209
|
content=new_body,
|
|
213
210
|
status_code=response.status_code,
|
|
@@ -215,12 +212,12 @@ def setup_trace_id_handler(app):
|
|
|
215
212
|
media_type=response.media_type
|
|
216
213
|
)
|
|
217
214
|
response.headers["content-length"] = str(len(new_body))
|
|
218
|
-
response.headers["x-traceId-header"] = trace_id
|
|
219
|
-
# 恢复 CORS
|
|
215
|
+
response.headers["x-traceId-header"] = trace_id
|
|
216
|
+
# 恢复 CORS
|
|
220
217
|
for k, v in cors_headers.items():
|
|
221
218
|
response.headers[k] = v
|
|
222
219
|
except json.JSONDecodeError:
|
|
223
|
-
# 非 JSON
|
|
220
|
+
# 非 JSON 或解析失败,仅更新长度
|
|
224
221
|
response = Response(
|
|
225
222
|
content=response_body,
|
|
226
223
|
status_code=response.status_code,
|
|
@@ -229,11 +226,11 @@ def setup_trace_id_handler(app):
|
|
|
229
226
|
)
|
|
230
227
|
response.headers["content-length"] = str(
|
|
231
228
|
len(response_body))
|
|
232
|
-
response.headers["x-traceId-header"] = trace_id
|
|
229
|
+
response.headers["x-traceId-header"] = trace_id
|
|
233
230
|
for k, v in cors_headers.items():
|
|
234
231
|
response.headers[k] = v
|
|
235
232
|
else:
|
|
236
|
-
# 非 JSON
|
|
233
|
+
# 非 JSON 响应
|
|
237
234
|
response = Response(
|
|
238
235
|
content=response_body,
|
|
239
236
|
status_code=response.status_code,
|
|
@@ -242,23 +239,21 @@ def setup_trace_id_handler(app):
|
|
|
242
239
|
)
|
|
243
240
|
response.headers["content-length"] = str(
|
|
244
241
|
len(response_body))
|
|
245
|
-
response.headers["x-traceId-header"] = trace_id
|
|
242
|
+
response.headers["x-traceId-header"] = trace_id
|
|
246
243
|
for k, v in cors_headers.items():
|
|
247
244
|
response.headers[k] = v
|
|
248
245
|
except StopAsyncIteration:
|
|
249
246
|
pass
|
|
250
247
|
|
|
251
|
-
#
|
|
248
|
+
# 构建响应日志
|
|
252
249
|
response_message = {
|
|
253
|
-
"traceId": trace_id,
|
|
250
|
+
"traceId": trace_id,
|
|
254
251
|
"status_code": response.status_code,
|
|
255
252
|
"response_body": response_body.decode('utf-8', errors='ignore'),
|
|
256
253
|
}
|
|
257
|
-
|
|
258
|
-
response_message, ensure_ascii=False)
|
|
259
|
-
SYLogger.info(response_message_str)
|
|
254
|
+
SYLogger.info(json.dumps(response_message, ensure_ascii=False))
|
|
260
255
|
|
|
261
|
-
#
|
|
256
|
+
# 兜底:确保 Header 必有 TraceId
|
|
262
257
|
try:
|
|
263
258
|
response.headers["x-traceId-header"] = trace_id
|
|
264
259
|
except AttributeError:
|
|
@@ -268,22 +263,33 @@ def setup_trace_id_handler(app):
|
|
|
268
263
|
if hasattr(response, "init_headers"):
|
|
269
264
|
response.init_headers(new_headers)
|
|
270
265
|
|
|
266
|
+
# 如果执行到这里,说明一切正常,标记为无异常
|
|
267
|
+
had_exception = False
|
|
271
268
|
return response
|
|
269
|
+
|
|
272
270
|
except Exception as e:
|
|
273
|
-
#
|
|
271
|
+
# ========== 4. 异常处理阶段 ==========
|
|
272
|
+
# 记录中间件层面的异常日志
|
|
274
273
|
error_message = {
|
|
275
274
|
"traceId": trace_id,
|
|
276
|
-
"error": str(e),
|
|
275
|
+
"error": f"Middleware Error: {str(e)}",
|
|
277
276
|
"query_params": query_params,
|
|
278
277
|
"request_body": request_body,
|
|
279
278
|
"uploaded_files": files_info if files_info else None
|
|
280
279
|
}
|
|
281
|
-
|
|
282
|
-
SYLogger.error(
|
|
280
|
+
# 使用 SYLogger.error,由于处于 except 块,会自动捕获堆栈
|
|
281
|
+
SYLogger.error(error_message)
|
|
282
|
+
|
|
283
|
+
# 关键:重新抛出异常,让 Global Exception Handler 接管
|
|
284
|
+
# 此时 had_exception 仍为 True,finally 不会 reset,trace_id 得以保留
|
|
283
285
|
raise
|
|
286
|
+
|
|
284
287
|
finally:
|
|
285
|
-
#
|
|
286
|
-
|
|
287
|
-
|
|
288
|
+
# ========== 5. 清理阶段 ==========
|
|
289
|
+
# 只有在没有任何异常的情况下(had_exception=False),才手动清除上下文
|
|
290
|
+
if not had_exception:
|
|
291
|
+
SYLogger.reset_trace_id(token)
|
|
292
|
+
SYLogger.reset_headers(header_token)
|
|
293
|
+
# 如果 had_exception 为 True,这里什么都不做,保留 ContextVar 供 Exception Handler 读取
|
|
288
294
|
|
|
289
295
|
return app
|
|
@@ -7,11 +7,12 @@ import json
|
|
|
7
7
|
from typing import Optional
|
|
8
8
|
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
9
9
|
from sycommon.config.Config import Config
|
|
10
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
async def send_wechat_markdown_msg(
|
|
13
14
|
content: str,
|
|
14
|
-
webhook: str =
|
|
15
|
+
webhook: str = None
|
|
15
16
|
) -> Optional[dict]:
|
|
16
17
|
"""
|
|
17
18
|
异步发送企业微信Markdown格式的WebHook消息
|
|
@@ -50,13 +51,14 @@ async def send_wechat_markdown_msg(
|
|
|
50
51
|
response_text) if response_text else {}
|
|
51
52
|
|
|
52
53
|
if status == 200 and response_data.get("errcode") == 0:
|
|
53
|
-
|
|
54
|
+
SYLogger.info(f"消息发送成功: {response_data}")
|
|
54
55
|
return response_data
|
|
55
56
|
else:
|
|
56
|
-
|
|
57
|
+
SYLogger.info(
|
|
58
|
+
f"消息发送失败 - 状态码: {status}, 响应: {response_data}")
|
|
57
59
|
return None
|
|
58
60
|
except Exception as e:
|
|
59
|
-
|
|
61
|
+
SYLogger.info(f"错误:未知异常 - {str(e)}")
|
|
60
62
|
return None
|
|
61
63
|
|
|
62
64
|
|
|
@@ -71,10 +73,12 @@ async def send_webhook(error_info: dict = None, webhook: str = None):
|
|
|
71
73
|
try:
|
|
72
74
|
service_name = Config().config.get('Name', "未知服务")
|
|
73
75
|
env = Config().config.get('Nacos', {}).get('namespaceId', '未知环境')
|
|
76
|
+
webHook = Config().config.get('llm', {}).get('WebHook', '未知环境')
|
|
74
77
|
except Exception as e:
|
|
75
78
|
service_name = "未知服务"
|
|
76
79
|
env = "未知环境"
|
|
77
|
-
|
|
80
|
+
webHook = None
|
|
81
|
+
SYLogger.info(f"读取配置失败: {str(e)}")
|
|
78
82
|
|
|
79
83
|
start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
80
84
|
|
|
@@ -92,13 +96,14 @@ async def send_webhook(error_info: dict = None, webhook: str = None):
|
|
|
92
96
|
> 错误信息: <font color="danger">{error_msg}</font>
|
|
93
97
|
> 错误堆栈: {stack_trace}"""
|
|
94
98
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
if webhook or webHook:
|
|
100
|
+
result = await send_wechat_markdown_msg(
|
|
101
|
+
content=markdown_content,
|
|
102
|
+
webhook=webhook or webHook
|
|
103
|
+
)
|
|
104
|
+
SYLogger.info(f"通知发送结果: {result}")
|
|
105
|
+
else:
|
|
106
|
+
SYLogger.info("未设置企业微信WebHook")
|
|
102
107
|
|
|
103
108
|
|
|
104
109
|
def run(*args, webhook: str = None, **kwargs):
|
|
@@ -133,7 +138,7 @@ def run(*args, webhook: str = None, **kwargs):
|
|
|
133
138
|
masked_query = urlencode(query, doseq=True)
|
|
134
139
|
masked_webhook = urlunparse(
|
|
135
140
|
(parsed.scheme, parsed.netloc, parsed.path, parsed.params, masked_query, parsed.fragment))
|
|
136
|
-
|
|
141
|
+
SYLogger.info(f"自定义企业微信WebHook: {masked_webhook}")
|
|
137
142
|
|
|
138
143
|
# 初始化错误信息
|
|
139
144
|
error_info = None
|
|
@@ -146,10 +151,10 @@ def run(*args, webhook: str = None, **kwargs):
|
|
|
146
151
|
except KeyboardInterrupt:
|
|
147
152
|
# 处理用户手动中断(不算启动失败)
|
|
148
153
|
elapsed = (datetime.now() - start_time).total_seconds()
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
SYLogger.info(f"\n{'='*50}")
|
|
155
|
+
SYLogger.info(f"ℹ️ 应用被用户手动中断")
|
|
156
|
+
SYLogger.info(f"启动耗时: {elapsed:.2f} 秒")
|
|
157
|
+
SYLogger.info(f"{'='*50}\n")
|
|
153
158
|
sys.exit(0)
|
|
154
159
|
|
|
155
160
|
except Exception as e:
|
|
@@ -167,16 +172,16 @@ def run(*args, webhook: str = None, **kwargs):
|
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
# 打印错误信息
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
SYLogger.info(f"\n{'='*50}")
|
|
176
|
+
SYLogger.info(f"🚨 应用启动失败!")
|
|
177
|
+
SYLogger.info(f"失败时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
178
|
+
SYLogger.info(f"错误类型: {type(e).__name__}")
|
|
179
|
+
SYLogger.info(f"错误信息: {str(e)}")
|
|
180
|
+
SYLogger.info(f"\n📝 错误堆栈(关键):")
|
|
181
|
+
SYLogger.info(f"-"*50)
|
|
177
182
|
traceback.print_exc(file=sys.stdout)
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
SYLogger.info(f"\n⏱️ 启动耗时: {elapsed:.2f} 秒")
|
|
184
|
+
SYLogger.info(f"{'='*50}\n")
|
|
180
185
|
|
|
181
186
|
finally:
|
|
182
187
|
# 运行异步通知函数,传递自定义的webhook参数
|
|
@@ -186,7 +191,7 @@ def run(*args, webhook: str = None, **kwargs):
|
|
|
186
191
|
webhook=webhook
|
|
187
192
|
))
|
|
188
193
|
except Exception as e:
|
|
189
|
-
|
|
194
|
+
SYLogger.info(f"错误:异步通知失败 - {str(e)}")
|
|
190
195
|
# 启动失败时退出程序
|
|
191
196
|
sys.exit(1)
|
|
192
197
|
|
|
@@ -799,7 +799,7 @@ class RabbitMQService:
|
|
|
799
799
|
)
|
|
800
800
|
|
|
801
801
|
# 构建消息头
|
|
802
|
-
namespaceId = Config().config.get('Nacos', {}).get('namespaceId', '
|
|
802
|
+
namespaceId = Config().config.get('Nacos', {}).get('namespaceId', '')
|
|
803
803
|
tenant_id = "T000002" if namespaceId == "prod" or namespaceId == "wsuat1" else "T000003"
|
|
804
804
|
mq_header = {
|
|
805
805
|
"context": SsoUser(
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import sentry_sdk
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from sycommon.config.Config import Config
|
|
4
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
5
|
+
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
|
6
|
+
# from sentry_sdk.integrations.logging import LoggingIntegration
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def sy_sentry_init():
|
|
10
|
+
config = Config().config
|
|
11
|
+
server_name = config.get('Name', '')
|
|
12
|
+
environment = config.get('Nacos', {}).get('namespaceId', '')
|
|
13
|
+
sentry_configs = config.get('SentryConfig', [])
|
|
14
|
+
target_config = next(
|
|
15
|
+
(item for item in sentry_configs if item.get('name') == server_name), None)
|
|
16
|
+
target_dsn = target_config.get('dsn')
|
|
17
|
+
target_enable = target_config.get('enable')
|
|
18
|
+
current_version = datetime.now().strftime("%Y-%m-%d %H:%M:%S-version")
|
|
19
|
+
if target_config and target_dsn and target_enable:
|
|
20
|
+
try:
|
|
21
|
+
sentry_sdk.init(
|
|
22
|
+
dsn=target_dsn,
|
|
23
|
+
traces_sample_rate=1.0,
|
|
24
|
+
server_name=server_name,
|
|
25
|
+
environment=environment,
|
|
26
|
+
release=current_version,
|
|
27
|
+
integrations=[
|
|
28
|
+
FastApiIntegration(),
|
|
29
|
+
# LoggingIntegration(level=logging.INFO,
|
|
30
|
+
# event_level=logging.ERROR)
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
SYLogger.error(f"Sentry初始化失败: {str(e)}")
|
sycommon/services.py
CHANGED
|
@@ -12,6 +12,7 @@ from sycommon.models.mqlistener_config import RabbitMQListenerConfig
|
|
|
12
12
|
from sycommon.models.mqsend_config import RabbitMQSendConfig
|
|
13
13
|
from sycommon.rabbitmq.rabbitmq_service import RabbitMQService
|
|
14
14
|
from sycommon.tools.docs import custom_redoc_html, custom_swagger_ui_html
|
|
15
|
+
from sycommon.sentry.sy_sentry import sy_sentry_init
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class Services(metaclass=SingletonMeta):
|
|
@@ -87,6 +88,9 @@ class Services(metaclass=SingletonMeta):
|
|
|
87
88
|
if logging_service:
|
|
88
89
|
logging_service(config)
|
|
89
90
|
|
|
91
|
+
# 设置sentry
|
|
92
|
+
sy_sentry_init()
|
|
93
|
+
|
|
90
94
|
# ========== 处理数据库服务 ==========
|
|
91
95
|
# 清空之前的待执行列表(防止热重载时重复)
|
|
92
96
|
cls._pending_async_db_setup = []
|