aury-boot 0.0.39__py3-none-any.whl → 0.0.40__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.
- aury/boot/_version.py +2 -2
- aury/boot/application/adapter/http.py +17 -6
- aury/boot/application/app/base.py +1 -0
- aury/boot/application/app/components.py +81 -2
- aury/boot/application/config/settings.py +73 -0
- aury/boot/commands/init.py +20 -0
- aury/boot/commands/pkg.py +31 -1
- aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +1 -0
- aury/boot/commands/templates/project/aury_docs/18-monitoring-profiling.md.tpl +239 -0
- aury/boot/commands/templates/project/env_templates/monitoring.tpl +15 -0
- aury/boot/common/logging/setup.py +8 -3
- aury/boot/infrastructure/database/manager.py +6 -4
- aury/boot/infrastructure/monitoring/__init__.py +10 -2
- aury/boot/infrastructure/monitoring/alerting/notifiers/feishu.py +32 -16
- aury/boot/infrastructure/monitoring/alerting/notifiers/webhook.py +14 -13
- aury/boot/infrastructure/monitoring/profiling/__init__.py +573 -0
- aury/boot/infrastructure/scheduler/manager.py +15 -3
- aury/boot/toolkit/http/__init__.py +180 -85
- {aury_boot-0.0.39.dist-info → aury_boot-0.0.40.dist-info}/METADATA +10 -4
- {aury_boot-0.0.39.dist-info → aury_boot-0.0.40.dist-info}/RECORD +22 -20
- {aury_boot-0.0.39.dist-info → aury_boot-0.0.40.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.39.dist-info → aury_boot-0.0.40.dist-info}/entry_points.txt +0 -0
|
@@ -100,7 +100,7 @@ def register_log_sink(
|
|
|
100
100
|
level=level,
|
|
101
101
|
format=sink_format or default_format,
|
|
102
102
|
encoding="utf-8",
|
|
103
|
-
enqueue=
|
|
103
|
+
enqueue=_log_config.get("enqueue", False),
|
|
104
104
|
delay=True,
|
|
105
105
|
filter=sink_filter,
|
|
106
106
|
)
|
|
@@ -165,6 +165,7 @@ def setup_logging(
|
|
|
165
165
|
rotation_size: str = "50 MB",
|
|
166
166
|
enable_console: bool = True,
|
|
167
167
|
logger_levels: list[tuple[str, str]] | None = None,
|
|
168
|
+
enqueue: bool = False,
|
|
168
169
|
) -> None:
|
|
169
170
|
"""设置日志配置。
|
|
170
171
|
|
|
@@ -189,6 +190,9 @@ def setup_logging(
|
|
|
189
190
|
enable_console: 是否输出到控制台
|
|
190
191
|
logger_levels: 需要设置特定级别的 logger 列表,格式: [("name", "LEVEL"), ...]
|
|
191
192
|
例如: [("sse_starlette", "WARNING"), ("httpx", "INFO")]
|
|
193
|
+
enqueue: 是否启用多进程安全队列(默认 False)。
|
|
194
|
+
启用后日志通过 multiprocessing.Queue 传输,
|
|
195
|
+
可能导致事件循环阻塞。建议在 asyncio 应用中保持 False。
|
|
192
196
|
"""
|
|
193
197
|
log_level = log_level.upper()
|
|
194
198
|
log_dir = log_dir or "logs"
|
|
@@ -208,6 +212,7 @@ def setup_logging(
|
|
|
208
212
|
"log_dir": log_dir,
|
|
209
213
|
"rotation": rotation,
|
|
210
214
|
"retention_days": retention_days,
|
|
215
|
+
"enqueue": enqueue,
|
|
211
216
|
"initialized": True,
|
|
212
217
|
})
|
|
213
218
|
|
|
@@ -252,7 +257,7 @@ def setup_logging(
|
|
|
252
257
|
retention=f"{retention_days} days",
|
|
253
258
|
level=log_level, # >= INFO 都写入(包含 WARNING/ERROR/CRITICAL)
|
|
254
259
|
encoding="utf-8",
|
|
255
|
-
enqueue=
|
|
260
|
+
enqueue=enqueue,
|
|
256
261
|
filter=lambda record, c=ctx: (
|
|
257
262
|
record["extra"].get("service") == c
|
|
258
263
|
and not record["extra"].get("access", False)
|
|
@@ -271,7 +276,7 @@ def setup_logging(
|
|
|
271
276
|
retention=f"{retention_days} days",
|
|
272
277
|
level="ERROR",
|
|
273
278
|
encoding="utf-8",
|
|
274
|
-
enqueue=
|
|
279
|
+
enqueue=enqueue,
|
|
275
280
|
filter=lambda record, c=ctx: record["extra"].get("service") == c,
|
|
276
281
|
)
|
|
277
282
|
|
|
@@ -226,6 +226,9 @@ class DatabaseManager:
|
|
|
226
226
|
async def session(self) -> AsyncGenerator[AsyncSession]:
|
|
227
227
|
"""获取数据库会话(上下文管理器)。
|
|
228
228
|
|
|
229
|
+
连接校验由 pool_pre_ping=True 在引擎层自动处理,
|
|
230
|
+
无需手动检查。
|
|
231
|
+
|
|
229
232
|
Yields:
|
|
230
233
|
AsyncSession: 数据库会话
|
|
231
234
|
|
|
@@ -235,7 +238,6 @@ class DatabaseManager:
|
|
|
235
238
|
"""
|
|
236
239
|
session = self.session_factory()
|
|
237
240
|
try:
|
|
238
|
-
await self._check_session_connection(session)
|
|
239
241
|
yield session
|
|
240
242
|
except SQLAlchemyError as exc:
|
|
241
243
|
# 只捕获数据库相关异常
|
|
@@ -253,15 +255,15 @@ class DatabaseManager:
|
|
|
253
255
|
async def create_session(self) -> AsyncSession:
|
|
254
256
|
"""创建新的数据库会话(需要手动关闭)。
|
|
255
257
|
|
|
258
|
+
连接校验由 pool_pre_ping=True 在引擎层自动处理。
|
|
259
|
+
|
|
256
260
|
Returns:
|
|
257
261
|
AsyncSession: 数据库会话
|
|
258
262
|
|
|
259
263
|
注意:使用后需要手动调用 await session.close()
|
|
260
264
|
建议使用 session() 上下文管理器代替此方法。
|
|
261
265
|
"""
|
|
262
|
-
|
|
263
|
-
await self._check_session_connection(session)
|
|
264
|
-
return session
|
|
266
|
+
return self.session_factory()
|
|
265
267
|
|
|
266
268
|
async def get_session(self) -> AsyncGenerator[AsyncSession]:
|
|
267
269
|
"""FastAPI 依赖注入专用的会话获取器。
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import traceback
|
|
8
9
|
from abc import ABC, abstractmethod
|
|
9
10
|
from collections.abc import Callable
|
|
10
11
|
from functools import wraps
|
|
@@ -13,6 +14,11 @@ import time
|
|
|
13
14
|
from aury.boot.common.logging import logger
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
def _format_exception_stacktrace(exc: Exception) -> str:
|
|
18
|
+
"""格式化异常堆栈为字符串。"""
|
|
19
|
+
return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
|
20
|
+
|
|
21
|
+
|
|
16
22
|
class MonitorContext:
|
|
17
23
|
"""监控上下文。
|
|
18
24
|
|
|
@@ -262,6 +268,7 @@ async def _emit_http_exception_alert(
|
|
|
262
268
|
method=method,
|
|
263
269
|
error_type=type(exception).__name__,
|
|
264
270
|
error_message=str(exception),
|
|
271
|
+
stacktrace=_format_exception_stacktrace(exception),
|
|
265
272
|
)
|
|
266
273
|
except ImportError:
|
|
267
274
|
pass
|
|
@@ -369,8 +376,9 @@ class ErrorReporterComponent(MonitorComponent):
|
|
|
369
376
|
source="service",
|
|
370
377
|
duration=context.duration,
|
|
371
378
|
service=context.service_name,
|
|
372
|
-
|
|
373
|
-
|
|
379
|
+
error_type=type(context.exception).__name__,
|
|
380
|
+
error_message=str(context.exception),
|
|
381
|
+
stacktrace=_format_exception_stacktrace(context.exception),
|
|
374
382
|
)
|
|
375
383
|
except ImportError:
|
|
376
384
|
pass # alerting 模块未加载
|
|
@@ -11,7 +11,7 @@ import hmac
|
|
|
11
11
|
import time
|
|
12
12
|
from typing import TYPE_CHECKING, Any
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import aiohttp
|
|
15
15
|
|
|
16
16
|
from aury.boot.common.logging import logger
|
|
17
17
|
|
|
@@ -118,6 +118,24 @@ class FeishuNotifier(AlertNotifier):
|
|
|
118
118
|
details.append(f"**错误信息**: {notification.metadata['error_message']}")
|
|
119
119
|
if "task_name" in notification.metadata:
|
|
120
120
|
details.append(f"**任务**: {notification.metadata['task_name']}")
|
|
121
|
+
|
|
122
|
+
# 事件循环阻塞检测专用字段
|
|
123
|
+
if "blocked_ms" in notification.metadata:
|
|
124
|
+
details.append(f"**阻塞时间**: {notification.metadata['blocked_ms']:.0f}ms")
|
|
125
|
+
if "threshold_ms" in notification.metadata:
|
|
126
|
+
details.append(f"**阈值**: {notification.metadata['threshold_ms']:.0f}ms")
|
|
127
|
+
if "total_blocks" in notification.metadata:
|
|
128
|
+
details.append(f"**累计阻塞**: {notification.metadata['total_blocks']} 次")
|
|
129
|
+
if "block_rate" in notification.metadata:
|
|
130
|
+
details.append(f"**阻塞率**: {notification.metadata['block_rate']}")
|
|
131
|
+
if "process_stats" in notification.metadata:
|
|
132
|
+
stats = notification.metadata["process_stats"]
|
|
133
|
+
if stats:
|
|
134
|
+
stats_str = f"CPU {stats.get('cpu_percent', 'N/A')}%, "
|
|
135
|
+
stats_str += f"RSS {stats.get('memory_rss_mb', 'N/A')}MB, "
|
|
136
|
+
stats_str += f"线程 {stats.get('num_threads', 'N/A')}"
|
|
137
|
+
details.append(f"**进程状态**: {stats_str}")
|
|
138
|
+
|
|
121
139
|
# SQL 和堆栈单独处理
|
|
122
140
|
if "sql" in notification.metadata:
|
|
123
141
|
sql_content = notification.metadata["sql"]
|
|
@@ -155,11 +173,10 @@ class FeishuNotifier(AlertNotifier):
|
|
|
155
173
|
"content": f"**堆栈**:\n```python\n{stacktrace_content}\n```",
|
|
156
174
|
})
|
|
157
175
|
|
|
158
|
-
#
|
|
176
|
+
# 构建卡片消息(飞书自定义机器人格式)
|
|
159
177
|
card = {
|
|
160
178
|
"msg_type": "interactive",
|
|
161
179
|
"card": {
|
|
162
|
-
"schema": "2.0",
|
|
163
180
|
"config": {
|
|
164
181
|
"wide_screen_mode": True,
|
|
165
182
|
},
|
|
@@ -170,9 +187,7 @@ class FeishuNotifier(AlertNotifier):
|
|
|
170
187
|
"content": notification.title,
|
|
171
188
|
},
|
|
172
189
|
},
|
|
173
|
-
"
|
|
174
|
-
"elements": elements,
|
|
175
|
-
},
|
|
190
|
+
"elements": elements,
|
|
176
191
|
},
|
|
177
192
|
}
|
|
178
193
|
|
|
@@ -191,16 +206,17 @@ class FeishuNotifier(AlertNotifier):
|
|
|
191
206
|
message["sign"] = self._generate_sign(timestamp)
|
|
192
207
|
|
|
193
208
|
# 发送请求
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
209
|
+
timeout = aiohttp.ClientTimeout(total=10)
|
|
210
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
211
|
+
async with session.post(self.webhook, json=message) as response:
|
|
212
|
+
result = await response.json()
|
|
213
|
+
|
|
214
|
+
if result.get("code") == 0 or result.get("StatusCode") == 0:
|
|
215
|
+
logger.debug(f"飞书通知发送成功: {notification.title}")
|
|
216
|
+
return True
|
|
217
|
+
else:
|
|
218
|
+
logger.error(f"飞书通知发送失败: {result}")
|
|
219
|
+
return False
|
|
204
220
|
except Exception as e:
|
|
205
221
|
logger.error(f"飞书通知发送异常: {e}")
|
|
206
222
|
return False
|
|
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from typing import TYPE_CHECKING, Any
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import aiohttp
|
|
11
11
|
|
|
12
12
|
from aury.boot.common.logging import logger
|
|
13
13
|
|
|
@@ -87,21 +87,22 @@ class WebhookNotifier(AlertNotifier):
|
|
|
87
87
|
try:
|
|
88
88
|
payload = self._build_payload(notification)
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
timeout = aiohttp.ClientTimeout(total=self.timeout)
|
|
91
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
92
|
+
async with session.post(
|
|
92
93
|
self.url,
|
|
93
94
|
json=payload,
|
|
94
95
|
headers=self.headers,
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
96
|
+
) as response:
|
|
97
|
+
if response.status < 400:
|
|
98
|
+
logger.debug(f"Webhook 通知发送成功: {notification.title}")
|
|
99
|
+
return True
|
|
100
|
+
else:
|
|
101
|
+
text = await response.text()
|
|
102
|
+
logger.error(
|
|
103
|
+
f"Webhook 通知发送失败: {response.status} - {text}"
|
|
104
|
+
)
|
|
105
|
+
return False
|
|
105
106
|
except Exception as e:
|
|
106
107
|
logger.error(f"Webhook 通知发送异常: {e}")
|
|
107
108
|
return False
|