sycommon-python-lib 0.1.56__py3-none-any.whl → 0.1.56b2__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.
Files changed (40) hide show
  1. sycommon/config/Config.py +3 -24
  2. sycommon/llm/embedding.py +23 -78
  3. sycommon/llm/get_llm.py +164 -24
  4. sycommon/logging/kafka_log.py +433 -187
  5. sycommon/middleware/exception.py +16 -10
  6. sycommon/middleware/timeout.py +1 -2
  7. sycommon/middleware/traceid.py +76 -81
  8. sycommon/rabbitmq/rabbitmq_client.py +242 -232
  9. sycommon/rabbitmq/rabbitmq_pool.py +218 -278
  10. sycommon/rabbitmq/rabbitmq_service.py +843 -25
  11. sycommon/services.py +96 -122
  12. sycommon/synacos/nacos_service.py +779 -63
  13. sycommon/tools/merge_headers.py +0 -20
  14. sycommon/tools/snowflake.py +153 -101
  15. {sycommon_python_lib-0.1.56.dist-info → sycommon_python_lib-0.1.56b2.dist-info}/METADATA +8 -10
  16. {sycommon_python_lib-0.1.56.dist-info → sycommon_python_lib-0.1.56b2.dist-info}/RECORD +19 -40
  17. sycommon/config/LangfuseConfig.py +0 -15
  18. sycommon/config/SentryConfig.py +0 -13
  19. sycommon/llm/llm_tokens.py +0 -119
  20. sycommon/llm/struct_token.py +0 -192
  21. sycommon/llm/sy_langfuse.py +0 -103
  22. sycommon/llm/usage_token.py +0 -117
  23. sycommon/notice/__init__.py +0 -0
  24. sycommon/notice/uvicorn_monitor.py +0 -200
  25. sycommon/rabbitmq/rabbitmq_service_client_manager.py +0 -206
  26. sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +0 -73
  27. sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +0 -285
  28. sycommon/rabbitmq/rabbitmq_service_core.py +0 -117
  29. sycommon/rabbitmq/rabbitmq_service_producer_manager.py +0 -238
  30. sycommon/sentry/__init__.py +0 -0
  31. sycommon/sentry/sy_sentry.py +0 -35
  32. sycommon/synacos/nacos_client_base.py +0 -119
  33. sycommon/synacos/nacos_config_manager.py +0 -107
  34. sycommon/synacos/nacos_heartbeat_manager.py +0 -144
  35. sycommon/synacos/nacos_service_discovery.py +0 -157
  36. sycommon/synacos/nacos_service_registration.py +0 -270
  37. sycommon/tools/env.py +0 -62
  38. {sycommon_python_lib-0.1.56.dist-info → sycommon_python_lib-0.1.56b2.dist-info}/WHEEL +0 -0
  39. {sycommon_python_lib-0.1.56.dist-info → sycommon_python_lib-0.1.56b2.dist-info}/entry_points.txt +0 -0
  40. {sycommon_python_lib-0.1.56.dist-info → sycommon_python_lib-0.1.56b2.dist-info}/top_level.txt +0 -0
@@ -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
- from sycommon.logging.kafka_log import SYLogger
4
+ import traceback
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.', 'traceId': SYLogger.get_trace_id()},
18
+ 'code': 413, 'error': f'File size exceeds the allowed limit of {int_MaxBytes}MB.'},
19
19
  status_code=413
20
20
  )
21
21
 
@@ -27,8 +27,7 @@ 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),
31
- "traceId": SYLogger.get_trace_id()
30
+ "path": str(request.url.path)
32
31
  }
33
32
  )
34
33
 
@@ -40,8 +39,7 @@ def setup_exception_handler(app, config: dict):
40
39
  content={
41
40
  "code": 400,
42
41
  "message": "参数验证失败",
43
- "details": exc.errors(),
44
- "traceId": SYLogger.get_trace_id()
42
+ "details": exc.errors()
45
43
  }
46
44
  )
47
45
 
@@ -57,22 +55,30 @@ def setup_exception_handler(app, config: dict):
57
55
  status_code=exc.code,
58
56
  content={
59
57
  "code": exc.code,
60
- "message": exc.message,
61
- "traceId": SYLogger.get_trace_id()
58
+ "message": exc.message
62
59
  }
63
60
  )
64
61
 
65
62
  # 5. 全局异常处理器(捕获所有未处理的异常)
66
63
  @app.exception_handler(Exception)
67
64
  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
+
68
75
  # 返回统一格式的错误响应(生产环境可选择不返回详细信息)
69
76
  return JSONResponse(
70
77
  status_code=500,
71
78
  content={
72
79
  "code": 500,
73
80
  "message": "服务器内部错误,请稍后重试",
74
- "detail": str(exc) if config.get('DEBUG', False) else "Internal Server Error",
75
- "traceId": SYLogger.get_trace_id()
81
+ "detail": str(exc) if config.get('DEBUG', False) else "Internal Server Error"
76
82
  }
77
83
  )
78
84
 
@@ -2,7 +2,6 @@
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
6
5
 
7
6
 
8
7
  def setup_request_timeout_middleware(app, config: dict):
@@ -15,6 +14,6 @@ def setup_request_timeout_middleware(app, config: dict):
15
14
  response = await call_next(request)
16
15
  duration = time.time() - request.state.start_time
17
16
  if duration > REQUEST_TIMEOUT:
18
- return JSONResponse(content={'code': 1, 'error': 'Request timed out', 'traceId': SYLogger.get_trace_id()}, status_code=504)
17
+ return JSONResponse(content={'code': 1, 'error': 'Request timed out'}, status_code=504)
19
18
  return response
20
19
  return app
@@ -10,8 +10,11 @@ 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. 请求阶段:获取/生成 TraceID ==========
14
- trace_id = request.headers.get("x-traceid-header")
13
+ # ========== 1. 请求阶段:确保获取/生成 x-traceId-header ==========
14
+ # 优先从请求头读取(兼容任意大小写)
15
+ trace_id = request.headers.get(
16
+ "x-traceId-header") or request.headers.get("x-traceid-header")
17
+ # 无则生成雪花ID
15
18
  if not trace_id:
16
19
  trace_id = Snowflake.id
17
20
 
@@ -34,30 +37,38 @@ def setup_trace_id_handler(app):
34
37
 
35
38
  if is_json_content and request.method in ["POST", "PUT", "PATCH"]:
36
39
  try:
37
- # 兼容纯文本格式的 JSON
40
+ # 兼容纯文本格式的 JSON(先读文本再解析)
38
41
  if "text/plain" in content_type:
39
42
  raw_text = await request.text(encoding="utf-8")
40
43
  request_body = json.loads(raw_text)
41
44
  else:
45
+ # application/json 直接解析
42
46
  request_body = await request.json()
43
- except Exception:
47
+ except Exception as e:
44
48
  try:
45
49
  request_body = await request.json()
46
50
  except Exception as e:
51
+ # 精准捕获 JSON 解析错误(而非泛 Exception)
47
52
  request_body = {"error": f"JSON parse failed: {str(e)}"}
48
53
 
49
54
  elif "multipart/form-data" in content_type and request.method in ["POST", "PUT"]:
50
55
  try:
56
+ # 从请求头中提取boundary
51
57
  boundary = None
52
58
  if "boundary=" in content_type:
53
59
  boundary = content_type.split("boundary=")[1].strip()
54
60
  boundary = boundary.encode('ascii')
55
61
 
56
62
  if boundary:
63
+ # 读取原始请求体
57
64
  body = await request.body()
65
+
66
+ # 尝试从原始请求体中提取文件名
58
67
  parts = body.split(boundary)
59
68
  for part in parts:
60
69
  part_str = part.decode('utf-8', errors='ignore')
70
+
71
+ # 使用正则表达式查找文件名
61
72
  filename_match = re.search(
62
73
  r'filename="([^"]+)"', part_str)
63
74
  if filename_match:
@@ -71,69 +82,62 @@ def setup_trace_id_handler(app):
71
82
  request_body = {
72
83
  "error": f"Failed to process form data: {str(e)}"}
73
84
 
74
- # 构建请求日志
85
+ # 构建请求日志(包含 traceId)
75
86
  request_message = {
76
- "traceId": trace_id,
87
+ "traceId": trace_id, # 请求日志中加入 traceId
77
88
  "method": request.method,
78
89
  "url": str(request.url),
79
90
  "query_params": query_params,
80
91
  "request_body": request_body,
81
92
  "uploaded_files": files_info if files_info else None
82
93
  }
83
- SYLogger.info(json.dumps(request_message, ensure_ascii=False))
84
-
85
- # 标记位:默认认为会发生异常
86
- # 这样如果中途代码报错跳转到 except,finally 就不会 reset,保留 trace_id 给 Exception Handler
87
- had_exception = True
94
+ request_message_str = json.dumps(request_message, ensure_ascii=False)
95
+ SYLogger.info(request_message_str)
88
96
 
89
97
  try:
90
- # ========== 2. 处理请求 ==========
98
+ # 处理请求
91
99
  response = await call_next(request)
92
100
 
93
- # ========== 3. 响应处理阶段 ==========
94
- # 注意:此阶段发生的任何异常都会被下方的 except 捕获
95
- # 从而保证 trace_id 不被清除,能够透传
96
-
97
- response_content_type = response.headers.get(
98
- "content-type", "").lower()
101
+ # 获取响应Content-Type(统一小写)
102
+ content_type = response.headers.get("content-type", "").lower()
99
103
 
100
- # 处理 SSE (Server-Sent Events)
101
- if "text/event-stream" in response_content_type:
104
+ # ========== 2. SSE 响应:仅设置 x-traceId-header,不修改其他头 ==========
105
+ if "text/event-stream" in content_type:
102
106
  try:
103
- response.headers["x-traceid-header"] = trace_id
107
+ # 强制写入 x-traceId-header 到响应头
108
+ response.headers["x-traceId-header"] = trace_id
109
+ # 确保前端能读取(仅补充暴露头,不覆盖原有值)
104
110
  expose_headers = response.headers.get(
105
111
  "access-control-expose-headers", "")
106
112
  if expose_headers:
107
- if "x-traceid-header" not in expose_headers.lower():
113
+ if "x-traceId-header" not in expose_headers.lower():
108
114
  response.headers[
109
- "access-control-expose-headers"] = f"{expose_headers}, x-traceid-header"
115
+ "access-control-expose-headers"] = f"{expose_headers}, x-traceId-header"
110
116
  else:
111
- response.headers["access-control-expose-headers"] = "x-traceid-header"
112
-
113
- # SSE 必须移除 Content-Length
117
+ response.headers["access-control-expose-headers"] = "x-traceId-header"
118
+ # SSE 必须移除 Content-Length(仅这一个额外操作)
114
119
  headers_lower = {
115
120
  k.lower(): k for k in response.headers.keys()}
116
121
  if "content-length" in headers_lower:
117
122
  del response.headers[headers_lower["content-length"]]
118
123
  except AttributeError:
119
- # 流式响应头只读处理
124
+ # 流式响应头只读:初始化时仅加入 traceId 和必要暴露头
120
125
  new_headers = dict(response.headers) if hasattr(
121
126
  response.headers, 'items') else {}
122
- new_headers["x-traceid-header"] = trace_id
127
+ new_headers["x-traceId-header"] = trace_id # 强制加入
128
+ # 保留原有暴露头,补充 traceId
123
129
  if "access-control-expose-headers" in new_headers:
124
- if "x-traceid-header" not in new_headers["access-control-expose-headers"].lower():
125
- new_headers["access-control-expose-headers"] += ", x-traceid-header"
130
+ if "x-traceId-header" not in new_headers["access-control-expose-headers"].lower():
131
+ new_headers["access-control-expose-headers"] += ", x-traceId-header"
126
132
  else:
127
- new_headers["access-control-expose-headers"] = "x-traceid-header"
133
+ new_headers["access-control-expose-headers"] = "x-traceId-header"
134
+ # 移除 Content-Length
128
135
  new_headers.pop("content-length", None)
129
136
  response.init_headers(new_headers)
130
-
131
- # SSE 不处理 Body,直接返回
132
- had_exception = False
133
137
  return response
134
138
 
135
- # 处理非 SSE 响应
136
- # 备份 CORS
139
+ # ========== 3. 非 SSE 响应:强制写入 x-traceId-header,保留 CORS ==========
140
+ # 备份 CORS 头(防止丢失)
137
141
  cors_headers = {}
138
142
  cors_header_keys = [
139
143
  "access-control-allow-origin",
@@ -149,7 +153,7 @@ def setup_trace_id_handler(app):
149
153
  cors_headers[key] = response.headers[k]
150
154
  break
151
155
 
152
- # 合并 Headers
156
+ # 合并 headers(非 SSE 场景)
153
157
  merged_headers = merge_headers(
154
158
  source_headers=request.headers,
155
159
  target_headers=response.headers,
@@ -157,20 +161,19 @@ def setup_trace_id_handler(app):
157
161
  delete_keys={'content-length', 'accept', 'content-type'}
158
162
  )
159
163
 
160
- # 强制加入 x-traceid-header
161
- merged_headers["x-traceid-header"] = trace_id
164
+ # 强制加入 x-traceId-header(优先级最高)
165
+ merged_headers["x-traceId-header"] = trace_id
166
+ # 恢复 CORS 头 + 补充 traceId 到暴露头
162
167
  merged_headers.update(cors_headers)
163
-
164
- # 更新暴露头
165
168
  expose_headers = merged_headers.get(
166
169
  "access-control-expose-headers", "")
167
170
  if expose_headers:
168
- if "x-traceid-header" not in expose_headers.lower():
169
- merged_headers["access-control-expose-headers"] = f"{expose_headers}, x-traceid-header"
171
+ if "x-traceId-header" not in expose_headers.lower():
172
+ merged_headers["access-control-expose-headers"] = f"{expose_headers}, x-traceId-header"
170
173
  else:
171
- merged_headers["access-control-expose-headers"] = "x-traceid-header"
174
+ merged_headers["access-control-expose-headers"] = "x-traceId-header"
172
175
 
173
- # 应用 Headers
176
+ # 更新响应头
174
177
  if hasattr(response.headers, 'clear'):
175
178
  response.headers.clear()
176
179
  for k, v in merged_headers.items():
@@ -184,26 +187,27 @@ def setup_trace_id_handler(app):
184
187
  except (AttributeError, KeyError):
185
188
  pass
186
189
 
187
- # 处理响应体
190
+ # 处理普通响应体(JSON 加入 traceId)
188
191
  response_body = b""
189
192
  try:
190
193
  async for chunk in response.body_iterator:
191
194
  response_body += chunk
192
195
 
196
+ # 获取 Content-Disposition(统一小写)
193
197
  content_disposition = response.headers.get(
194
198
  "content-disposition", "").lower()
195
199
 
196
- # JSON 响应体注入 traceId
197
- if "application/json" in response_content_type and not content_disposition.startswith("attachment"):
200
+ # JSON 响应体加入 traceId
201
+ if "application/json" in content_type and not content_disposition.startswith("attachment"):
198
202
  try:
199
203
  data = json.loads(response_body)
200
204
  new_body = response_body
201
- if isinstance(data, dict):
202
- data["traceId"] = trace_id
205
+ if data:
206
+ data["traceId"] = trace_id # 响应体也加入
203
207
  new_body = json.dumps(
204
208
  data, ensure_ascii=False).encode()
205
209
 
206
- # 重建 Response 以更新 Body 和 Content-Length
210
+ # 重建响应,确保 header 包含 x-traceId-header
207
211
  response = Response(
208
212
  content=new_body,
209
213
  status_code=response.status_code,
@@ -211,12 +215,12 @@ def setup_trace_id_handler(app):
211
215
  media_type=response.media_type
212
216
  )
213
217
  response.headers["content-length"] = str(len(new_body))
214
- response.headers["x-traceid-header"] = trace_id
215
- # 恢复 CORS
218
+ response.headers["x-traceId-header"] = trace_id # 再次兜底
219
+ # 恢复 CORS
216
220
  for k, v in cors_headers.items():
217
221
  response.headers[k] = v
218
222
  except json.JSONDecodeError:
219
- # 非 JSON 或解析失败,仅更新长度
223
+ # 非 JSON 响应:仅更新长度,强制加入 traceId
220
224
  response = Response(
221
225
  content=response_body,
222
226
  status_code=response.status_code,
@@ -225,11 +229,11 @@ def setup_trace_id_handler(app):
225
229
  )
226
230
  response.headers["content-length"] = str(
227
231
  len(response_body))
228
- response.headers["x-traceid-header"] = trace_id
232
+ response.headers["x-traceId-header"] = trace_id # 强制加入
229
233
  for k, v in cors_headers.items():
230
234
  response.headers[k] = v
231
235
  else:
232
- # 非 JSON 响应
236
+ # 非 JSON 响应:强制加入 traceId
233
237
  response = Response(
234
238
  content=response_body,
235
239
  status_code=response.status_code,
@@ -238,57 +242,48 @@ def setup_trace_id_handler(app):
238
242
  )
239
243
  response.headers["content-length"] = str(
240
244
  len(response_body))
241
- response.headers["x-traceid-header"] = trace_id
245
+ response.headers["x-traceId-header"] = trace_id # 强制加入
242
246
  for k, v in cors_headers.items():
243
247
  response.headers[k] = v
244
248
  except StopAsyncIteration:
245
249
  pass
246
250
 
247
- # 构建响应日志
251
+ # 构建响应日志(包含 traceId)
248
252
  response_message = {
249
- "traceId": trace_id,
253
+ "traceId": trace_id, # 响应日志加入 traceId
250
254
  "status_code": response.status_code,
251
255
  "response_body": response_body.decode('utf-8', errors='ignore'),
252
256
  }
253
- SYLogger.info(json.dumps(response_message, ensure_ascii=False))
257
+ response_message_str = json.dumps(
258
+ response_message, ensure_ascii=False)
259
+ SYLogger.info(response_message_str)
254
260
 
255
- # 兜底:确保 Header 必有 TraceId
261
+ # ========== 最终兜底:确保响应头必有 x-traceId-header ==========
256
262
  try:
257
- response.headers["x-traceid-header"] = trace_id
263
+ response.headers["x-traceId-header"] = trace_id
258
264
  except AttributeError:
259
265
  new_headers = dict(response.headers) if hasattr(
260
266
  response.headers, 'items') else {}
261
- new_headers["x-traceid-header"] = trace_id
267
+ new_headers["x-traceId-header"] = trace_id
262
268
  if hasattr(response, "init_headers"):
263
269
  response.init_headers(new_headers)
264
270
 
265
- # 如果执行到这里,说明一切正常,标记为无异常
266
- had_exception = False
267
271
  return response
268
-
269
272
  except Exception as e:
270
- # ========== 4. 异常处理阶段 ==========
271
- # 记录中间件层面的异常日志
273
+ # 异常日志也加入 traceId
272
274
  error_message = {
273
275
  "traceId": trace_id,
274
- "error": f"Middleware Error: {str(e)}",
276
+ "error": str(e),
275
277
  "query_params": query_params,
276
278
  "request_body": request_body,
277
279
  "uploaded_files": files_info if files_info else None
278
280
  }
279
- # 使用 SYLogger.error,由于处于 except 块,会自动捕获堆栈
280
- SYLogger.error(error_message)
281
-
282
- # 关键:重新抛出异常,让 Global Exception Handler 接管
283
- # 此时 had_exception 仍为 True,finally 不会 reset,trace_id 得以保留
281
+ error_message_str = json.dumps(error_message, ensure_ascii=False)
282
+ SYLogger.error(error_message_str)
284
283
  raise
285
-
286
284
  finally:
287
- # ========== 5. 清理阶段 ==========
288
- # 只有在没有任何异常的情况下(had_exception=False),才手动清除上下文
289
- if not had_exception:
290
- SYLogger.reset_trace_id(token)
291
- SYLogger.reset_headers(header_token)
292
- # 如果 had_exception 为 True,这里什么都不做,保留 ContextVar 供 Exception Handler 读取
285
+ # 清理上下文变量
286
+ SYLogger.reset_trace_id(token)
287
+ SYLogger.reset_headers(header_token)
293
288
 
294
289
  return app