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.
@@ -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=True,
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=True,
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=True,
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
- session = self.session_factory()
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
- exception_type=type(context.exception).__name__,
373
- exception_message=str(context.exception),
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 httpx
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
- # 构建 JSON 2.0 卡片消息
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
- "body": {
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
- async with httpx.AsyncClient(timeout=10) as client:
195
- response = await client.post(self.webhook, json=message)
196
- result = response.json()
197
-
198
- if result.get("code") == 0 or result.get("StatusCode") == 0:
199
- logger.debug(f"飞书通知发送成功: {notification.title}")
200
- return True
201
- else:
202
- logger.error(f"飞书通知发送失败: {result}")
203
- return False
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 httpx
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
- async with httpx.AsyncClient(timeout=self.timeout) as client:
91
- response = await client.post(
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
- if response.is_success:
98
- logger.debug(f"Webhook 通知发送成功: {notification.title}")
99
- return True
100
- else:
101
- logger.error(
102
- f"Webhook 通知发送失败: {response.status_code} - {response.text}"
103
- )
104
- return False
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