sycommon-python-lib 0.1.58__tar.gz → 0.2.0b0__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.
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/PKG-INFO +2 -1
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/pyproject.toml +2 -1
- sycommon_python_lib-0.2.0b0/src/sycommon/notice/uvicorn_monitor.py +188 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/rabbitmq/rabbitmq_client.py +104 -66
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/rabbitmq/rabbitmq_pool.py +120 -59
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/synacos/nacos_service_discovery.py +9 -6
- sycommon_python_lib-0.2.0b0/src/sycommon/tests/test_email.py +172 -0
- sycommon_python_lib-0.2.0b0/src/sycommon/tools/syemail.py +173 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon_python_lib.egg-info/PKG-INFO +2 -1
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon_python_lib.egg-info/SOURCES.txt +2 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon_python_lib.egg-info/requires.txt +1 -0
- sycommon_python_lib-0.1.58/src/sycommon/notice/uvicorn_monitor.py +0 -200
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/README.md +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/setup.cfg +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/command/cli.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/__init__.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/config/Config.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/config/DatabaseConfig.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/config/EmbeddingConfig.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/config/LLMConfig.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/config/LangfuseConfig.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/config/MQConfig.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/config/RerankerConfig.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/config/SentryConfig.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/config/__init__.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/database/async_base_db_service.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/database/async_database_service.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/database/base_db_service.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/database/database_service.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/health/__init__.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/health/health_check.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/health/metrics.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/health/ping.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/llm/__init__.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/llm/embedding.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/llm/get_llm.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/llm/llm_logger.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/llm/llm_tokens.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/llm/struct_token.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/llm/sy_langfuse.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/llm/usage_token.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/logging/__init__.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/logging/async_sql_logger.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/logging/kafka_log.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/logging/logger_levels.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/logging/logger_wrapper.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/logging/sql_logger.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/middleware/__init__.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/middleware/context.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/middleware/cors.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/middleware/docs.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/middleware/exception.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/middleware/middleware.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/middleware/monitor_memory.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/middleware/mq.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/middleware/timeout.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/middleware/traceid.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/models/__init__.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/models/base_http.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/models/log.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/models/mqlistener_config.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/models/mqmsg_model.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/models/mqsend_config.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/models/sso_user.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/notice/__init__.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/rabbitmq/rabbitmq_service.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/rabbitmq/rabbitmq_service_client_manager.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/rabbitmq/rabbitmq_service_core.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/rabbitmq/rabbitmq_service_producer_manager.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/sentry/__init__.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/sentry/sy_sentry.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/services.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/sse/__init__.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/sse/event.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/sse/sse.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/synacos/__init__.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/synacos/example.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/synacos/example2.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/synacos/feign.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/synacos/feign_client.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/synacos/nacos_client_base.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/synacos/nacos_config_manager.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/synacos/nacos_heartbeat_manager.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/synacos/nacos_service.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/synacos/nacos_service_registration.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/synacos/param.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/tools/__init__.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/tools/docs.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/tools/env.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/tools/merge_headers.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/tools/snowflake.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/tools/timing.py +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
- {sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon_python_lib.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sycommon-python-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0b0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -21,6 +21,7 @@ Requires-Dist: nacos-sdk-python<3.0,>=2.0.9
|
|
|
21
21
|
Requires-Dist: psutil>=7.2.1
|
|
22
22
|
Requires-Dist: pydantic>=2.12.5
|
|
23
23
|
Requires-Dist: python-dotenv>=1.2.1
|
|
24
|
+
Requires-Dist: python-multipart>=0.0.21
|
|
24
25
|
Requires-Dist: pyyaml>=6.0.3
|
|
25
26
|
Requires-Dist: sentry-sdk[fastapi]>=2.49.0
|
|
26
27
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.45
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sycommon-python-lib"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0b0"
|
|
4
4
|
description = "Add your description here"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -22,6 +22,7 @@ dependencies = [
|
|
|
22
22
|
"psutil>=7.2.1",
|
|
23
23
|
"pydantic>=2.12.5",
|
|
24
24
|
"python-dotenv>=1.2.1",
|
|
25
|
+
"python-multipart>=0.0.21",
|
|
25
26
|
"pyyaml>=6.0.3",
|
|
26
27
|
"sentry-sdk[fastapi]>=2.49.0",
|
|
27
28
|
"sqlalchemy[asyncio]>=2.0.45",
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import traceback
|
|
3
|
+
import aiohttp
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
8
|
+
from sycommon.config.Config import Config
|
|
9
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def send_wechat_markdown_msg(
|
|
13
|
+
content: str,
|
|
14
|
+
webhook: str = None
|
|
15
|
+
) -> Optional[dict]:
|
|
16
|
+
"""
|
|
17
|
+
异步发送企业微信Markdown格式的WebHook消息
|
|
18
|
+
"""
|
|
19
|
+
# 构造请求体(Markdown格式)
|
|
20
|
+
payload = {
|
|
21
|
+
"msgtype": "markdown",
|
|
22
|
+
"markdown": {
|
|
23
|
+
"content": content
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# 使用 json 参数自动处理序列化和 Content-Type
|
|
29
|
+
async with aiohttp.ClientSession() as session:
|
|
30
|
+
async with session.post(
|
|
31
|
+
url=webhook,
|
|
32
|
+
json=payload,
|
|
33
|
+
timeout=aiohttp.ClientTimeout(total=10)
|
|
34
|
+
) as response:
|
|
35
|
+
response_data = await response.json()
|
|
36
|
+
|
|
37
|
+
if response.status == 200 and response_data.get("errcode") == 0:
|
|
38
|
+
SYLogger.info(f"消息发送成功: {response_data}")
|
|
39
|
+
return response_data
|
|
40
|
+
else:
|
|
41
|
+
SYLogger.warning(
|
|
42
|
+
f"消息发送失败 - 状态码: {response.status}, 响应: {response_data}")
|
|
43
|
+
return None
|
|
44
|
+
except Exception as e:
|
|
45
|
+
SYLogger.error(f"发送企业微信消息异常: {str(e)}")
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def send_webhook(error_info: dict = None, webhook: str = None):
|
|
50
|
+
"""
|
|
51
|
+
发送服务启动结果的企业微信通知
|
|
52
|
+
"""
|
|
53
|
+
# 获取服务名和环境(增加默认值保护)
|
|
54
|
+
try:
|
|
55
|
+
config = Config().config
|
|
56
|
+
service_name = config.get('Name', "未知服务")
|
|
57
|
+
env = config.get('Nacos', {}).get('namespaceId', '未知环境')
|
|
58
|
+
# 注意:这里使用了大写开头的 WebHook,请确保配置文件中键名一致
|
|
59
|
+
webHook = config.get('llm', {}).get('WebHook')
|
|
60
|
+
except Exception as e:
|
|
61
|
+
service_name = "未知服务"
|
|
62
|
+
env = "未知环境"
|
|
63
|
+
webHook = None
|
|
64
|
+
SYLogger.warning(f"读取配置失败: {str(e)}")
|
|
65
|
+
|
|
66
|
+
start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
67
|
+
|
|
68
|
+
# 如果没有错误信息,就不发送失败告警
|
|
69
|
+
if not error_info:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# 启动失败的通知内容(包含详细错误信息)
|
|
73
|
+
error_type = error_info.get("error_type", "未知错误")
|
|
74
|
+
error_msg = error_info.get("error_msg", "无错误信息")
|
|
75
|
+
stack_trace = error_info.get("stack_trace", "无堆栈信息")[:1000] # 适当增加长度限制
|
|
76
|
+
elapsed_time = error_info.get("elapsed_time", 0)
|
|
77
|
+
|
|
78
|
+
markdown_content = f"""### {service_name}服务启动失败告警 ⚠️
|
|
79
|
+
> 环境: <font color="warning">{env}</font>
|
|
80
|
+
> 启动时间: <font color="comment">{start_time}</font>
|
|
81
|
+
> 耗时: <font color="comment">{elapsed_time:.2f}秒</font>
|
|
82
|
+
> 错误类型: <font color="danger">{error_type}</font>
|
|
83
|
+
> 错误信息: <font color="danger">{error_msg}</font>
|
|
84
|
+
> 错误堆栈: {stack_trace}"""
|
|
85
|
+
|
|
86
|
+
if webhook or webHook:
|
|
87
|
+
result = await send_wechat_markdown_msg(
|
|
88
|
+
content=markdown_content,
|
|
89
|
+
webhook=webhook or webHook
|
|
90
|
+
)
|
|
91
|
+
SYLogger.info(f"通知发送结果: {result}")
|
|
92
|
+
else:
|
|
93
|
+
SYLogger.info("未设置企业微信WebHook,跳过告警发送")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def run(*args, webhook: str = None, **kwargs):
|
|
97
|
+
"""
|
|
98
|
+
带企业微信告警的Uvicorn启动监控
|
|
99
|
+
"""
|
|
100
|
+
# 判断环境
|
|
101
|
+
try:
|
|
102
|
+
env = Config().config.get('Nacos', {}).get('namespaceId', 'dev')
|
|
103
|
+
except:
|
|
104
|
+
env = 'dev'
|
|
105
|
+
|
|
106
|
+
# 如果是生产环境,通常不需要这种启动脚本进行告警干扰,或者你需要根据实际情况调整
|
|
107
|
+
if env == "prod":
|
|
108
|
+
import uvicorn
|
|
109
|
+
uvicorn.run(*args, **kwargs)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# 记录启动开始时间
|
|
113
|
+
start_time = datetime.now()
|
|
114
|
+
|
|
115
|
+
if webhook:
|
|
116
|
+
# 脱敏展示webhook
|
|
117
|
+
try:
|
|
118
|
+
parsed = urlparse(webhook)
|
|
119
|
+
query = parse_qs(parsed.query)
|
|
120
|
+
if 'key' in query and query['key'][0]:
|
|
121
|
+
key = query['key'][0]
|
|
122
|
+
masked_key = key[:8] + "****" if len(key) > 8 else key + "****"
|
|
123
|
+
query['key'] = [masked_key]
|
|
124
|
+
masked_query = urlencode(query, doseq=True)
|
|
125
|
+
masked_webhook = urlunparse(
|
|
126
|
+
(parsed.scheme, parsed.netloc, parsed.path, parsed.params, masked_query, parsed.fragment))
|
|
127
|
+
SYLogger.info(f"自定义企业微信WebHook: {masked_webhook}")
|
|
128
|
+
except Exception:
|
|
129
|
+
SYLogger.info("自定义Webhook格式解析失败,但会尝试使用")
|
|
130
|
+
|
|
131
|
+
# 初始化错误信息
|
|
132
|
+
error_info = None
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
import uvicorn
|
|
136
|
+
# 执行启动
|
|
137
|
+
uvicorn.run(*args, **kwargs)
|
|
138
|
+
|
|
139
|
+
except KeyboardInterrupt:
|
|
140
|
+
# 处理用户手动中断
|
|
141
|
+
elapsed = (datetime.now() - start_time).total_seconds()
|
|
142
|
+
SYLogger.info(f"\n{'='*50}")
|
|
143
|
+
SYLogger.info(f"ℹ️ 应用被用户手动中断")
|
|
144
|
+
SYLogger.info(f"启动耗时: {elapsed:.2f} 秒")
|
|
145
|
+
SYLogger.info(f"{'='*50}\n")
|
|
146
|
+
# 手动中断不需要错误告警,也不需要 sys.exit(1)
|
|
147
|
+
sys.exit(0)
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
# 捕获启动失败异常
|
|
151
|
+
elapsed = (datetime.now() - start_time).total_seconds()
|
|
152
|
+
stack_trace = traceback.format_exc()
|
|
153
|
+
|
|
154
|
+
error_info = {
|
|
155
|
+
"error_type": type(e).__name__,
|
|
156
|
+
"error_msg": str(e),
|
|
157
|
+
"stack_trace": stack_trace,
|
|
158
|
+
"elapsed_time": elapsed
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# 打印详细错误
|
|
162
|
+
SYLogger.error(f"\n{'='*50}")
|
|
163
|
+
SYLogger.error(f"🚨 应用启动失败!")
|
|
164
|
+
SYLogger.error(f"失败时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
165
|
+
SYLogger.error(f"错误类型: {type(e).__name__}")
|
|
166
|
+
SYLogger.error(f"错误信息: {str(e)}")
|
|
167
|
+
SYLogger.error(f"\n📝 错误堆栈:")
|
|
168
|
+
SYLogger.error(f"-"*50)
|
|
169
|
+
traceback.print_exc(file=sys.stdout)
|
|
170
|
+
SYLogger.error(f"\n⏱️ 启动耗时: {elapsed:.2f} 秒")
|
|
171
|
+
SYLogger.error(f"{'='*50}\n")
|
|
172
|
+
|
|
173
|
+
finally:
|
|
174
|
+
# 创建新的事件循环来发送通知,防止与 Uvicorn 的循环冲突
|
|
175
|
+
if error_info:
|
|
176
|
+
try:
|
|
177
|
+
loop = asyncio.new_event_loop()
|
|
178
|
+
asyncio.set_event_loop(loop)
|
|
179
|
+
loop.run_until_complete(send_webhook(
|
|
180
|
+
error_info=error_info,
|
|
181
|
+
webhook=webhook
|
|
182
|
+
))
|
|
183
|
+
loop.close()
|
|
184
|
+
except Exception as e:
|
|
185
|
+
SYLogger.error(f"错误:异步通知发送失败 - {str(e)}")
|
|
186
|
+
|
|
187
|
+
# 只有确实有错误时才以状态码 1 退出
|
|
188
|
+
sys.exit(1)
|
{sycommon_python_lib-0.1.58 → sycommon_python_lib-0.2.0b0}/src/sycommon/rabbitmq/rabbitmq_client.py
RENAMED
|
@@ -117,7 +117,7 @@ class RabbitMQClient:
|
|
|
117
117
|
logger.info(f"队列重建成功: {self.queue_name}")
|
|
118
118
|
|
|
119
119
|
async def connect(self) -> None:
|
|
120
|
-
"""
|
|
120
|
+
"""连接方法"""
|
|
121
121
|
if self._closed:
|
|
122
122
|
raise RuntimeError("客户端已关闭,无法重新连接")
|
|
123
123
|
|
|
@@ -125,9 +125,10 @@ class RabbitMQClient:
|
|
|
125
125
|
await self._connect_condition.acquire()
|
|
126
126
|
|
|
127
127
|
try:
|
|
128
|
-
# ===== 阶段 A:
|
|
128
|
+
# ===== 阶段 A: 检查状态与排队 =====
|
|
129
129
|
if await self.is_connected:
|
|
130
|
-
self._connect_condition.
|
|
130
|
+
if self._connect_condition.locked():
|
|
131
|
+
self._connect_condition.release()
|
|
131
132
|
return
|
|
132
133
|
|
|
133
134
|
if self._connecting:
|
|
@@ -135,19 +136,16 @@ class RabbitMQClient:
|
|
|
135
136
|
logger.debug("连接正在进行中,等待现有连接完成...")
|
|
136
137
|
await asyncio.wait_for(self._connect_condition.wait(), timeout=60.0)
|
|
137
138
|
except asyncio.TimeoutError:
|
|
138
|
-
|
|
139
|
-
raise RuntimeError("等待连接超时")
|
|
139
|
+
logger.warning("等待前序连接超时,当前协程将尝试强制接管并重连...")
|
|
140
140
|
|
|
141
|
+
# 唤醒后再次检查
|
|
141
142
|
if await self.is_connected:
|
|
142
|
-
self._connect_condition.
|
|
143
|
+
if self._connect_condition.locked():
|
|
144
|
+
self._connect_condition.release()
|
|
143
145
|
return
|
|
144
|
-
else:
|
|
145
|
-
self._connect_condition.release()
|
|
146
|
-
raise RuntimeError("等待重连后,连接状态依然无效")
|
|
147
146
|
|
|
148
|
-
# ===== 阶段 B:
|
|
147
|
+
# ===== 阶段 B: 标记开始连接并释放锁 =====
|
|
149
148
|
self._connecting = True
|
|
150
|
-
# 【关键】释放锁,允许其他协程进入等待逻辑
|
|
151
149
|
self._connect_condition.release()
|
|
152
150
|
|
|
153
151
|
except Exception as e:
|
|
@@ -155,13 +153,18 @@ class RabbitMQClient:
|
|
|
155
153
|
self._connect_condition.release()
|
|
156
154
|
raise
|
|
157
155
|
|
|
158
|
-
# === 阶段 C: 执行耗时的连接逻辑 (
|
|
156
|
+
# === 阶段 C: 执行耗时的连接逻辑 (无锁状态) ===
|
|
157
|
+
connection_failed = False
|
|
158
|
+
was_consuming = False
|
|
159
|
+
|
|
160
|
+
# 用于追踪状态,避免在 except 中访问 self._x 导致的竞态
|
|
161
|
+
old_channel = self._channel
|
|
162
|
+
|
|
159
163
|
try:
|
|
160
|
-
# --- 步骤 1:
|
|
161
|
-
# 必须在清理前记录状态
|
|
164
|
+
# --- 步骤 1: 记录状态并清理旧资源 ---
|
|
162
165
|
was_consuming = self._consumer_tag is not None
|
|
163
166
|
|
|
164
|
-
#
|
|
167
|
+
# 清理旧连接回调
|
|
165
168
|
if self._channel_conn:
|
|
166
169
|
try:
|
|
167
170
|
if self._channel_conn.close_callbacks:
|
|
@@ -169,7 +172,14 @@ class RabbitMQClient:
|
|
|
169
172
|
except Exception:
|
|
170
173
|
pass
|
|
171
174
|
|
|
172
|
-
#
|
|
175
|
+
# 显式关闭旧 Channel(这是 Client 自己创建的资源,必须关)
|
|
176
|
+
if old_channel and not old_channel.is_closed:
|
|
177
|
+
try:
|
|
178
|
+
await old_channel.close()
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
# 重置引用
|
|
173
183
|
self._channel = None
|
|
174
184
|
self._channel_conn = None
|
|
175
185
|
self._exchange = None
|
|
@@ -177,52 +187,68 @@ class RabbitMQClient:
|
|
|
177
187
|
self._consumer_tag = None
|
|
178
188
|
|
|
179
189
|
# --- 步骤 2: 获取新连接 ---
|
|
190
|
+
# 注意:如果这里抛出异常,说明 Pool 层面连接失败
|
|
180
191
|
self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
|
|
181
192
|
|
|
182
|
-
#
|
|
193
|
+
# --- 步骤 3: 设置回调 ---
|
|
194
|
+
loop = asyncio.get_running_loop()
|
|
195
|
+
|
|
183
196
|
def on_conn_closed(conn, exc):
|
|
197
|
+
if self._closed:
|
|
198
|
+
return
|
|
184
199
|
logger.warning(f"检测到底层连接关闭: {exc}")
|
|
185
|
-
|
|
186
|
-
asyncio.create_task(self._safe_reconnect())
|
|
200
|
+
asyncio.run_coroutine_threadsafe(self._safe_reconnect(), loop)
|
|
187
201
|
|
|
188
202
|
if self._channel_conn:
|
|
189
203
|
self._channel_conn.close_callbacks.add(on_conn_closed)
|
|
190
204
|
|
|
191
|
-
# --- 步骤
|
|
205
|
+
# --- 步骤 4: 重建基础资源 ---
|
|
192
206
|
await self._rebuild_resources()
|
|
193
207
|
|
|
194
|
-
# --- 步骤 4: 恢复消费 ---
|
|
195
|
-
if was_consuming and self._message_handler:
|
|
196
|
-
logger.info("🔄 检测到重连前处于消费状态,尝试自动恢复消费...")
|
|
197
|
-
try:
|
|
198
|
-
# 直接调用 start_consuming 来恢复,它内部包含了完整的队列检查和绑定逻辑
|
|
199
|
-
self._consumer_tag = await self.start_consuming()
|
|
200
|
-
logger.info(f"✅ 消费已自动恢复: {self._consumer_tag}")
|
|
201
|
-
except Exception as e:
|
|
202
|
-
logger.error(f"❌ 自动恢复消费失败: {e}")
|
|
203
|
-
self._consumer_tag = None
|
|
204
|
-
|
|
205
|
-
logger.info("客户端连接初始化完成")
|
|
206
|
-
|
|
207
208
|
except Exception as e:
|
|
209
|
+
connection_failed = True
|
|
208
210
|
logger.error(f"客户端连接失败: {str(e)}", exc_info=True)
|
|
209
|
-
|
|
211
|
+
|
|
212
|
+
# 清理引用
|
|
210
213
|
if self._channel_conn and self._channel_conn.close_callbacks:
|
|
211
214
|
self._channel_conn.close_callbacks.clear()
|
|
215
|
+
|
|
212
216
|
self._channel = None
|
|
213
217
|
self._channel_conn = None
|
|
218
|
+
self._exchange = None
|
|
214
219
|
self._queue = None
|
|
215
220
|
self._consumer_tag = None
|
|
221
|
+
|
|
222
|
+
# 不要手动关闭 Pool 返回的连接,只置空引用。
|
|
216
223
|
raise
|
|
217
224
|
|
|
218
225
|
finally:
|
|
219
|
-
# === 阶段 D:
|
|
220
|
-
|
|
226
|
+
# === 阶段 D: 恢复消费与收尾 (重新加锁) ===
|
|
227
|
+
# 确保一定会获取锁
|
|
228
|
+
try:
|
|
229
|
+
await self._connect_condition.acquire()
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
|
|
221
233
|
try:
|
|
234
|
+
# 只有连接完全成功,且之前在消费,才尝试恢复消费
|
|
235
|
+
if not connection_failed and was_consuming and self._message_handler:
|
|
236
|
+
logger.info("🔄 检测到重连前处于消费状态,尝试自动恢复消费...")
|
|
237
|
+
try:
|
|
238
|
+
self._consumer_tag = await self.start_consuming()
|
|
239
|
+
logger.info(f"✅ 消费已自动恢复: {self._consumer_tag}")
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.error(f"❌ 自动恢复消费失败: {e}")
|
|
242
|
+
# 如果消费恢复失败,视为连接状态不完整,置空 Exchange
|
|
243
|
+
self._consumer_tag = None
|
|
244
|
+
self._exchange = None
|
|
245
|
+
finally:
|
|
246
|
+
# 最终状态复位
|
|
222
247
|
self._connecting = False
|
|
223
248
|
self._connect_condition.notify_all()
|
|
224
|
-
|
|
225
|
-
self._connect_condition.
|
|
249
|
+
|
|
250
|
+
if self._connect_condition.locked():
|
|
251
|
+
self._connect_condition.release()
|
|
226
252
|
|
|
227
253
|
async def _safe_reconnect(self):
|
|
228
254
|
"""安全重连任务(仅用于被动监听连接关闭)"""
|
|
@@ -257,32 +283,14 @@ class RabbitMQClient:
|
|
|
257
283
|
|
|
258
284
|
async def _process_message_callback(self, message: AbstractIncomingMessage):
|
|
259
285
|
try:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
body_dict = json.loads(message.body.decode("utf-8"))
|
|
266
|
-
msg_obj = MQMsgModel(**body_dict)
|
|
267
|
-
if not msg_obj.traceId:
|
|
268
|
-
msg_obj.traceId = message.headers.get(
|
|
269
|
-
"trace-id") if message.headers else SYLogger.get_trace_id()
|
|
270
|
-
except json.JSONDecodeError as e:
|
|
271
|
-
logger.error(f"JSON解析失败: {e}")
|
|
272
|
-
await message.reject(requeue=False)
|
|
273
|
-
return
|
|
274
|
-
else:
|
|
275
|
-
msg_obj = MQMsgModel(
|
|
276
|
-
body=message.body.decode("utf-8"),
|
|
277
|
-
routing_key=message.routing_key,
|
|
278
|
-
delivery_tag=message.delivery_tag,
|
|
279
|
-
traceId=message.headers.get(
|
|
280
|
-
"trace-id") if message.headers else SYLogger.get_trace_id(),
|
|
281
|
-
)
|
|
286
|
+
body_dict = json.loads(message.body.decode("utf-8"))
|
|
287
|
+
msg_obj: MQMsgModel = MQMsgModel(**body_dict)
|
|
288
|
+
if not msg_obj.traceId:
|
|
289
|
+
msg_obj.traceId = message.headers.get(
|
|
290
|
+
"trace-id") if message.headers else SYLogger.get_trace_id()
|
|
282
291
|
|
|
283
292
|
SYLogger.set_trace_id(msg_obj.traceId)
|
|
284
293
|
|
|
285
|
-
# 3. 执行业务逻辑
|
|
286
294
|
if self._message_handler:
|
|
287
295
|
await self._message_handler(msg_obj, message)
|
|
288
296
|
|
|
@@ -331,6 +339,11 @@ class RabbitMQClient:
|
|
|
331
339
|
self._consumer_tag = None
|
|
332
340
|
|
|
333
341
|
async def _handle_publish_failure(self):
|
|
342
|
+
# 如果当前正在重连,或者已经关闭,直接返回,避免冲突
|
|
343
|
+
if self._connecting or self._closed:
|
|
344
|
+
logger.warning("⚠️ 正在重连或已关闭,跳过故障转移触发")
|
|
345
|
+
return
|
|
346
|
+
|
|
334
347
|
try:
|
|
335
348
|
logger.info("检测到发布异常,强制连接池切换节点...")
|
|
336
349
|
await self.connection_pool.force_reconnect()
|
|
@@ -405,9 +418,12 @@ class RabbitMQClient:
|
|
|
405
418
|
raise RuntimeError(f"消息发布最终失败: {last_exception}")
|
|
406
419
|
|
|
407
420
|
async def close(self) -> None:
|
|
421
|
+
"""关闭客户端(整合版:修复连接泄漏与死锁)"""
|
|
422
|
+
# 1. 先标记关闭,这会阻止 _safe_reconnect 和后续的 connect 逻辑
|
|
408
423
|
self._closed = True
|
|
409
424
|
logger.info("开始关闭RabbitMQ客户端...")
|
|
410
425
|
|
|
426
|
+
# 2. 取消可能存在的后台重连任务
|
|
411
427
|
if self._current_reconnect_task and not self._current_reconnect_task.done():
|
|
412
428
|
self._current_reconnect_task.cancel()
|
|
413
429
|
try:
|
|
@@ -415,21 +431,43 @@ class RabbitMQClient:
|
|
|
415
431
|
except asyncio.CancelledError:
|
|
416
432
|
pass
|
|
417
433
|
|
|
434
|
+
# 3. 停止消费
|
|
418
435
|
await self.stop_consuming()
|
|
419
436
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
437
|
+
# 4. 【关键步骤】处理 _connect_condition 锁
|
|
438
|
+
# 我们必须获取这个锁,以防止正在进行的 connect() 在我们清理资源时还在操作
|
|
439
|
+
# 但如果 connect 卡在 wait(),我们需要强制唤醒它
|
|
440
|
+
try:
|
|
441
|
+
# 尝试获取锁,设置超时防止死锁(虽然理论上我们即将 notify_all,但为了保险)
|
|
442
|
+
await asyncio.wait_for(self._connect_condition.acquire(), timeout=2.0)
|
|
443
|
+
except asyncio.TimeoutError:
|
|
444
|
+
logger.warning("获取连接锁超时,强制清理资源...")
|
|
445
|
+
|
|
446
|
+
try:
|
|
447
|
+
# 清理回调,防止在关闭过程中触发重连
|
|
448
|
+
if self._channel_conn:
|
|
449
|
+
try:
|
|
450
|
+
if self._channel_conn.close_callbacks:
|
|
451
|
+
self._channel_conn.close_callbacks.clear()
|
|
452
|
+
except Exception:
|
|
453
|
+
pass
|
|
424
454
|
|
|
455
|
+
# 置空资源引用
|
|
425
456
|
self._channel = None
|
|
426
457
|
self._channel_conn = None
|
|
427
458
|
self._exchange = None
|
|
428
459
|
self._queue = None
|
|
429
460
|
self._message_handler = None
|
|
461
|
+
self._conn_close_callback = None
|
|
430
462
|
|
|
431
|
-
|
|
463
|
+
finally:
|
|
464
|
+
# 【核心修复】无论是否成功获取锁,都要强制重置状态并唤醒所有等待者
|
|
465
|
+
# 这会让卡在 connect() 阶段 A 的 wait() 的协程醒来,发现 _closed=True 后抛出异常退出
|
|
432
466
|
self._connecting = False
|
|
433
467
|
self._connect_condition.notify_all()
|
|
434
468
|
|
|
469
|
+
# 确保锁被释放(如果持有)
|
|
470
|
+
if self._connect_condition.locked():
|
|
471
|
+
self._connect_condition.release()
|
|
472
|
+
|
|
435
473
|
logger.info("客户端已关闭")
|