aury-boot 0.0.29__py3-none-any.whl → 0.0.31__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 (54) hide show
  1. aury/boot/_version.py +2 -2
  2. aury/boot/application/__init__.py +2 -4
  3. aury/boot/application/app/base.py +126 -2
  4. aury/boot/application/app/components.py +226 -1
  5. aury/boot/application/config/settings.py +201 -3
  6. aury/boot/application/constants/components.py +3 -0
  7. aury/boot/application/middleware/logging.py +45 -6
  8. aury/boot/commands/docs.py +40 -0
  9. aury/boot/commands/init.py +2 -0
  10. aury/boot/commands/templates/project/AGENTS.md.tpl +59 -0
  11. aury/boot/commands/templates/project/alert_rules.example.yaml.tpl +85 -0
  12. aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +3 -0
  13. aury/boot/commands/templates/project/aury_docs/17-alerting.md.tpl +210 -0
  14. aury/boot/commands/templates/project/env_templates/messaging.tpl +21 -13
  15. aury/boot/commands/templates/project/env_templates/monitoring.tpl +63 -0
  16. aury/boot/common/logging/context.py +17 -1
  17. aury/boot/common/logging/format.py +4 -0
  18. aury/boot/infrastructure/__init__.py +4 -8
  19. aury/boot/infrastructure/channel/__init__.py +9 -8
  20. aury/boot/infrastructure/channel/backends/__init__.py +2 -6
  21. aury/boot/infrastructure/channel/backends/broadcaster.py +141 -0
  22. aury/boot/infrastructure/channel/base.py +11 -4
  23. aury/boot/infrastructure/channel/manager.py +25 -24
  24. aury/boot/infrastructure/database/query_tools/__init__.py +3 -5
  25. aury/boot/infrastructure/events/__init__.py +4 -6
  26. aury/boot/infrastructure/events/backends/__init__.py +2 -4
  27. aury/boot/infrastructure/events/backends/broadcaster.py +189 -0
  28. aury/boot/infrastructure/events/base.py +9 -4
  29. aury/boot/infrastructure/events/manager.py +24 -20
  30. aury/boot/infrastructure/monitoring/__init__.py +210 -6
  31. aury/boot/infrastructure/monitoring/alerting/__init__.py +50 -0
  32. aury/boot/infrastructure/monitoring/alerting/aggregator.py +193 -0
  33. aury/boot/infrastructure/monitoring/alerting/events.py +141 -0
  34. aury/boot/infrastructure/monitoring/alerting/manager.py +430 -0
  35. aury/boot/infrastructure/monitoring/alerting/notifiers/__init__.py +16 -0
  36. aury/boot/infrastructure/monitoring/alerting/notifiers/base.py +60 -0
  37. aury/boot/infrastructure/monitoring/alerting/notifiers/feishu.py +209 -0
  38. aury/boot/infrastructure/monitoring/alerting/notifiers/webhook.py +110 -0
  39. aury/boot/infrastructure/monitoring/alerting/rules.py +179 -0
  40. aury/boot/infrastructure/monitoring/health/__init__.py +231 -0
  41. aury/boot/infrastructure/monitoring/tracing/__init__.py +55 -0
  42. aury/boot/infrastructure/monitoring/tracing/context.py +43 -0
  43. aury/boot/infrastructure/monitoring/tracing/logging.py +73 -0
  44. aury/boot/infrastructure/monitoring/tracing/processor.py +357 -0
  45. aury/boot/infrastructure/monitoring/tracing/provider.py +322 -0
  46. aury/boot/infrastructure/monitoring/tracing/tracing.py +235 -0
  47. {aury_boot-0.0.29.dist-info → aury_boot-0.0.31.dist-info}/METADATA +14 -1
  48. {aury_boot-0.0.29.dist-info → aury_boot-0.0.31.dist-info}/RECORD +50 -33
  49. aury/boot/infrastructure/channel/backends/memory.py +0 -126
  50. aury/boot/infrastructure/channel/backends/redis.py +0 -130
  51. aury/boot/infrastructure/events/backends/memory.py +0 -86
  52. aury/boot/infrastructure/events/backends/redis.py +0 -169
  53. {aury_boot-0.0.29.dist-info → aury_boot-0.0.31.dist-info}/WHEEL +0 -0
  54. {aury_boot-0.0.29.dist-info → aury_boot-0.0.31.dist-info}/entry_points.txt +0 -0
@@ -9,8 +9,7 @@ from collections.abc import AsyncIterator
9
9
 
10
10
  from aury.boot.common.logging import logger
11
11
 
12
- from .backends.memory import MemoryChannel
13
- from .backends.redis import RedisChannel
12
+ from .backends.broadcaster import BroadcasterChannel
14
13
  from .base import ChannelBackend, ChannelMessage, IChannel
15
14
 
16
15
 
@@ -51,7 +50,6 @@ class ChannelManager:
51
50
  self._backend: IChannel | None = None
52
51
  self._backend_type: ChannelBackend | None = None
53
52
  self._initialized: bool = False
54
- self._redis_client = None
55
53
  self._url: str | None = None
56
54
 
57
55
  @classmethod
@@ -84,17 +82,19 @@ class ChannelManager:
84
82
 
85
83
  async def initialize(
86
84
  self,
87
- backend: ChannelBackend | str = ChannelBackend.MEMORY,
85
+ backend: ChannelBackend | str = ChannelBackend.BROADCASTER,
88
86
  *,
89
- url: str | None = None,
90
- max_subscribers: int = 1000,
87
+ url: str = "memory://",
91
88
  ) -> ChannelManager:
92
89
  """初始化通道(链式调用)。
93
90
 
94
91
  Args:
95
- backend: 后端类型
96
- url: Redis 连接 URL(当 backend=redis 时需要)
97
- max_subscribers: 内存后端的最大订阅者数量
92
+ backend: 后端类型,默认 broadcaster
93
+ url: 连接 URL,支持:
94
+ - memory:// - 内存后端(单进程,默认)
95
+ - redis://host:port/db - Redis Pub/Sub
96
+ - kafka://host:port - Apache Kafka
97
+ - postgres://user:pass@host/db - PostgreSQL
98
98
 
99
99
  Returns:
100
100
  self: 支持链式调用
@@ -110,24 +110,28 @@ class ChannelManager:
110
110
  self._backend_type = backend
111
111
  self._url = url
112
112
 
113
- if backend == ChannelBackend.MEMORY:
114
- self._backend = MemoryChannel(max_subscribers=max_subscribers)
115
- elif backend == ChannelBackend.REDIS:
116
- if url is None:
117
- raise ValueError("Redis 通道需要提供 url 参数")
118
- # 内部创建 RedisClient
119
- from aury.boot.infrastructure.clients.redis import RedisClient
120
-
121
- self._redis_client = RedisClient()
122
- await self._redis_client.configure(url=url).initialize()
123
- self._backend = RedisChannel(self._redis_client)
113
+ if backend == ChannelBackend.BROADCASTER:
114
+ self._backend = BroadcasterChannel(url)
115
+ elif backend in (ChannelBackend.RABBITMQ, ChannelBackend.ROCKETMQ):
116
+ raise NotImplementedError(f"{backend.value} 后端暂未实现")
124
117
  else:
125
118
  raise ValueError(f"不支持的通道后端: {backend}")
126
119
 
127
120
  self._initialized = True
128
- logger.info(f"通道管理器 [{self.name}] 初始化完成: {backend.value}")
121
+ logger.info(f"通道管理器 [{self.name}] 初始化完成: {backend.value}, url={self._mask_url(url)}")
129
122
  return self
130
123
 
124
+ def _mask_url(self, url: str) -> str:
125
+ """URL 脱敏(隐藏密码)。"""
126
+ if "@" in url:
127
+ parts = url.split("@")
128
+ prefix = parts[0]
129
+ suffix = parts[1]
130
+ if ":" in prefix:
131
+ scheme_and_user = prefix.rsplit(":", 1)[0]
132
+ return f"{scheme_and_user}:***@{suffix}"
133
+ return url
134
+
131
135
  @property
132
136
  def backend(self) -> IChannel:
133
137
  """获取通道后端。"""
@@ -218,9 +222,6 @@ class ChannelManager:
218
222
  if self._backend:
219
223
  await self._backend.close()
220
224
  self._backend = None
221
- if self._redis_client:
222
- await self._redis_client.cleanup()
223
- self._redis_client = None
224
225
  self._initialized = False
225
226
  logger.info(f"通道管理器 [{self.name}] 已关闭")
226
227
 
@@ -12,8 +12,8 @@ import time
12
12
 
13
13
  from aury.boot.common.logging import logger
14
14
 
15
- # 查询性能监控配置
16
- QUERY_SLOW_THRESHOLD = 1.0 # 慢查询阈值(秒)
15
+ # 默认慢查询阈值(秒)
16
+ DEFAULT_SLOW_QUERY_THRESHOLD = 1.0
17
17
 
18
18
 
19
19
  def cache_query(
@@ -86,7 +86,7 @@ def cache_query(
86
86
 
87
87
 
88
88
  def monitor_query(
89
- slow_threshold: float = QUERY_SLOW_THRESHOLD,
89
+ slow_threshold: float = DEFAULT_SLOW_QUERY_THRESHOLD,
90
90
  enable_explain: bool = False,
91
91
  ) -> Callable:
92
92
  """查询性能监控装饰器。
@@ -104,7 +104,6 @@ def monitor_query(
104
104
  async def list(self, **filters):
105
105
  return await super().list(**filters)
106
106
  """
107
-
108
107
  def decorator(func: Callable) -> Callable:
109
108
  @wraps(func)
110
109
  async def wrapper(self, *args, **kwargs):
@@ -154,7 +153,6 @@ def monitor_query(
154
153
 
155
154
  return decorator
156
155
 
157
-
158
156
  __all__ = [
159
157
  "cache_query",
160
158
  "monitor_query",
@@ -3,12 +3,11 @@
3
3
  提供发布/订阅模式的事件总线功能,用于模块间解耦通信。
4
4
 
5
5
  支持的后端:
6
- - memory: 内存事件总线(单进程)
7
- - redis: Redis Pub/Sub(多进程/多实例)
8
- - rabbitmq: RabbitMQ Exchange(分布式)
6
+ - broadcaster: 基于 broadcaster 库(推荐,支持 memory/redis/kafka/postgres)
7
+ - rabbitmq: RabbitMQ Exchange(复杂消息场景)
9
8
  """
10
9
 
11
- from .backends import MemoryEventBus, RabbitMQEventBus, RedisEventBus
10
+ from .backends import BroadcasterEventBus, RabbitMQEventBus
12
11
  from .base import Event, EventBackend, EventHandler, EventType, IEventBus
13
12
  from .manager import EventBusManager
14
13
 
@@ -22,9 +21,8 @@ __all__ = [
22
21
  "EventType",
23
22
  "IEventBus",
24
23
  # 后端实现
25
- "MemoryEventBus",
24
+ "BroadcasterEventBus",
26
25
  "RabbitMQEventBus",
27
- "RedisEventBus",
28
26
  ]
29
27
 
30
28
 
@@ -1,11 +1,9 @@
1
1
  """事件总线后端实现。"""
2
2
 
3
- from .memory import MemoryEventBus
3
+ from .broadcaster import BroadcasterEventBus
4
4
  from .rabbitmq import RabbitMQEventBus
5
- from .redis import RedisEventBus
6
5
 
7
6
  __all__ = [
8
- "MemoryEventBus",
7
+ "BroadcasterEventBus",
9
8
  "RabbitMQEventBus",
10
- "RedisEventBus",
11
9
  ]
@@ -0,0 +1,189 @@
1
+ """Broadcaster 事件总线后端。
2
+
3
+ 使用 broadcaster 库实现事件发布/订阅,支持多种后端:
4
+ - memory:// 内存(单进程)
5
+ - redis:// Redis Pub/Sub(多进程/多实例)
6
+ - kafka:// Apache Kafka
7
+ - postgres:// PostgreSQL LISTEN/NOTIFY
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import json
14
+
15
+ from broadcaster import Broadcast
16
+
17
+ from aury.boot.common.logging import logger
18
+
19
+ from ..base import Event, EventHandler, IEventBus
20
+
21
+ # 框架默认前缀
22
+ DEFAULT_CHANNEL_PREFIX = "aury:event:"
23
+
24
+
25
+ class BroadcasterEventBus(IEventBus):
26
+ """Broadcaster 事件总线实现。
27
+
28
+ 使用 broadcaster 库实现事件发布/订阅。
29
+
30
+ 频道命名格式:{channel_prefix}{event_name}
31
+ 默认:aury:event:user.created
32
+
33
+ 优点:
34
+ - 统一接口支持多种后端
35
+ - 内置连接池管理
36
+ - 自动重连机制
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ url: str,
42
+ *,
43
+ channel_prefix: str = DEFAULT_CHANNEL_PREFIX,
44
+ ) -> None:
45
+ """初始化 Broadcaster 事件总线。
46
+
47
+ Args:
48
+ url: 连接 URL,格式:
49
+ - memory:// 内存(单进程)
50
+ - redis://host:port Redis Pub/Sub
51
+ - kafka://host:port Apache Kafka
52
+ - postgres://... PostgreSQL
53
+ channel_prefix: 频道名称前缀,默认 "aury:event:"
54
+
55
+ """
56
+ self._url = url
57
+ self._channel_prefix = channel_prefix
58
+ self._broadcast: Broadcast | None = None
59
+ # event_name -> list of handlers (本地订阅)
60
+ self._handlers: dict[str, list[EventHandler]] = {}
61
+ self._listener_tasks: dict[str, asyncio.Task] = {}
62
+ self._running = False
63
+
64
+ async def _ensure_connected(self) -> None:
65
+ """确保已连接。"""
66
+ if self._broadcast is None:
67
+ self._broadcast = Broadcast(self._url)
68
+ await self._broadcast.connect()
69
+ logger.debug(f"Broadcaster 事件总线已连接: {self._url}")
70
+
71
+ def _get_event_name(self, event_type: type[Event] | str) -> str:
72
+ """获取事件名称。"""
73
+ if isinstance(event_type, str):
74
+ return event_type
75
+ return event_type.__name__
76
+
77
+ def _get_channel(self, event_name: str) -> str:
78
+ """获取频道名称。"""
79
+ return f"{self._channel_prefix}{event_name}"
80
+
81
+ def subscribe(
82
+ self,
83
+ event_type: type[Event] | str,
84
+ handler: EventHandler,
85
+ ) -> None:
86
+ """订阅事件。
87
+
88
+ 注意:这是同步注册 handler,真正的监听在 start_listening() 中启动。
89
+ """
90
+ event_name = self._get_event_name(event_type)
91
+ if event_name not in self._handlers:
92
+ self._handlers[event_name] = []
93
+ if handler not in self._handlers[event_name]:
94
+ self._handlers[event_name].append(handler)
95
+ logger.debug(f"订阅事件: {event_name} -> {handler.__name__}")
96
+
97
+ # 如果已经在运行,立即为新事件启动监听
98
+ if self._running and event_name not in self._listener_tasks:
99
+ task = asyncio.create_task(self._listen_event(event_name))
100
+ self._listener_tasks[event_name] = task
101
+
102
+ def unsubscribe(
103
+ self,
104
+ event_type: type[Event] | str,
105
+ handler: EventHandler,
106
+ ) -> None:
107
+ """取消订阅事件。"""
108
+ event_name = self._get_event_name(event_type)
109
+ if event_name in self._handlers:
110
+ try:
111
+ self._handlers[event_name].remove(handler)
112
+ logger.debug(f"取消订阅事件: {event_name} -> {handler.__name__}")
113
+
114
+ # 如果该事件没有处理器了,停止监听
115
+ if not self._handlers[event_name] and event_name in self._listener_tasks:
116
+ self._listener_tasks[event_name].cancel()
117
+ del self._listener_tasks[event_name]
118
+ except ValueError:
119
+ pass
120
+
121
+ async def publish(self, event: Event) -> None:
122
+ """发布事件。"""
123
+ await self._ensure_connected()
124
+ event_name = event.event_name
125
+ channel = self._get_channel(event_name)
126
+ data = json.dumps(event.to_dict())
127
+ await self._broadcast.publish(channel=channel, message=data)
128
+
129
+ async def _listen_event(self, event_name: str) -> None:
130
+ """监听单个事件的消息。"""
131
+ channel = self._get_channel(event_name)
132
+ try:
133
+ async with self._broadcast.subscribe(channel=channel) as subscriber:
134
+ async for event_data in subscriber:
135
+ if not self._running:
136
+ break
137
+ try:
138
+ data = json.loads(event_data.message)
139
+ handlers = self._handlers.get(event_name, [])
140
+ for handler in handlers:
141
+ try:
142
+ event = Event.from_dict(data)
143
+ result = handler(event)
144
+ if asyncio.iscoroutine(result):
145
+ await result
146
+ except Exception as e:
147
+ logger.error(f"处理事件 {event_name} 失败: {e}")
148
+ except (json.JSONDecodeError, KeyError) as e:
149
+ logger.warning(f"解析事件消息失败: {e}")
150
+ except asyncio.CancelledError:
151
+ pass
152
+ except Exception as e:
153
+ logger.error(f"事件监听异常 {event_name}: {e}")
154
+
155
+ async def start_listening(self) -> None:
156
+ """开始监听事件(需要在后台任务中运行)。"""
157
+ if self._running:
158
+ return
159
+
160
+ await self._ensure_connected()
161
+ self._running = True
162
+
163
+ # 为每个已订阅的事件启动监听任务
164
+ for event_name in self._handlers:
165
+ if event_name not in self._listener_tasks:
166
+ task = asyncio.create_task(self._listen_event(event_name))
167
+ self._listener_tasks[event_name] = task
168
+
169
+ logger.debug(f"Broadcaster 事件总线开始监听,事件数: {len(self._handlers)}")
170
+
171
+ async def close(self) -> None:
172
+ """关闭事件总线。"""
173
+ self._running = False
174
+
175
+ # 取消所有监听任务
176
+ for task in self._listener_tasks.values():
177
+ task.cancel()
178
+ self._listener_tasks.clear()
179
+
180
+ # 关闭连接
181
+ if self._broadcast:
182
+ await self._broadcast.disconnect()
183
+ self._broadcast = None
184
+
185
+ self._handlers.clear()
186
+ logger.debug("Broadcaster 事件总线已关闭")
187
+
188
+
189
+ __all__ = ["BroadcasterEventBus"]
@@ -15,11 +15,16 @@ import uuid
15
15
 
16
16
 
17
17
  class EventBackend(Enum):
18
- """事件总线后端类型。"""
19
-
20
- MEMORY = "memory"
21
- REDIS = "redis"
18
+ """事件总线后端类型。
19
+
20
+ - BROADCASTER: 基于 broadcaster 库,支持 memory/redis/kafka/postgres
21
+ - RABBITMQ: 专用 RabbitMQ 实现(复杂消息场景)
22
+ - ROCKETMQ: 专用 RocketMQ 实现(预留)
23
+ """
24
+
25
+ BROADCASTER = "broadcaster"
22
26
  RABBITMQ = "rabbitmq"
27
+ ROCKETMQ = "rocketmq"
23
28
 
24
29
 
25
30
  @dataclass
@@ -10,14 +10,12 @@ from typing import TYPE_CHECKING, Any
10
10
 
11
11
  from aury.boot.common.logging import logger
12
12
 
13
- from .backends.memory import MemoryEventBus
13
+ from .backends.broadcaster import BroadcasterEventBus
14
14
  from .backends.rabbitmq import RabbitMQEventBus
15
- from .backends.redis import RedisEventBus
16
15
  from .base import Event, EventBackend, EventHandler, IEventBus
17
16
 
18
17
  if TYPE_CHECKING:
19
18
  from aury.boot.application.config import EventInstanceConfig
20
- from aury.boot.infrastructure.clients.redis import RedisClient
21
19
 
22
20
 
23
21
  class EventBusManager:
@@ -25,19 +23,19 @@ class EventBusManager:
25
23
 
26
24
  提供统一的事件总线管理接口,支持:
27
25
  - 多实例管理(如 local、distributed 各自独立)
28
- - 多后端支持(memoryredis、rabbitmq)
26
+ - 多后端支持(broadcaster、rabbitmq)
29
27
  - 发布/订阅模式
30
28
 
31
29
  使用示例:
32
30
  # 默认实例(内存)
33
31
  events = EventBusManager.get_instance()
34
- await events.initialize(backend="memory")
32
+ await events.initialize(backend="broadcaster", url="memory://")
35
33
 
36
- # 分布式实例
34
+ # 分布式实例(Redis)
37
35
  distributed = EventBusManager.get_instance("distributed")
38
36
  await distributed.initialize(
39
- backend="redis",
40
- redis_client=redis_client,
37
+ backend="broadcaster",
38
+ url="redis://localhost:6379/2",
41
39
  )
42
40
 
43
41
  # 订阅事件
@@ -92,10 +90,9 @@ class EventBusManager:
92
90
 
93
91
  async def initialize(
94
92
  self,
95
- backend: EventBackend | str = EventBackend.MEMORY,
93
+ backend: EventBackend | str = EventBackend.BROADCASTER,
96
94
  *,
97
95
  config: EventInstanceConfig | None = None,
98
- redis_client: RedisClient | None = None,
99
96
  url: str | None = None,
100
97
  channel_prefix: str | None = None,
101
98
  exchange_name: str = "aury.events",
@@ -105,9 +102,13 @@ class EventBusManager:
105
102
  Args:
106
103
  backend: 后端类型(当 config 不为 None 时忽略)
107
104
  config: Event 实例配置(推荐,自动根据 backend 初始化)
108
- redis_client: Redis 客户端(当 backend=redis 且 config=None 时需要)
109
- url: 连接 URL(当 config=None 时需要)
110
- channel_prefix: Redis 频道前缀,默认 "aury:event:"
105
+ url: 连接 URL,格式:
106
+ - memory:// 内存(单进程,默认)
107
+ - redis://host:port Redis Pub/Sub
108
+ - kafka://host:port Apache Kafka
109
+ - postgres://... PostgreSQL
110
+ - amqp://... RabbitMQ(需 backend=rabbitmq)
111
+ channel_prefix: 事件频道前缀,默认 "aury:event:"
111
112
  exchange_name: RabbitMQ 交换机名称,默认 "aury.events"
112
113
 
113
114
  Returns:
@@ -132,17 +133,20 @@ class EventBusManager:
132
133
 
133
134
  self._backend_type = backend
134
135
 
135
- # 根据后端类型创建实例,参数校验由后端自己处理
136
- if backend == EventBackend.MEMORY:
137
- self._backend = MemoryEventBus()
138
- elif backend == EventBackend.REDIS:
139
- # channel_prefix None 时使用 RedisEventBus 的默认值
140
- kwargs = {"url": url, "redis_client": redis_client}
136
+ # 根据后端类型创建实例
137
+ if backend == EventBackend.BROADCASTER:
138
+ # 默认使用内存
139
+ effective_url = url or "memory://"
140
+ kwargs: dict[str, Any] = {"url": effective_url}
141
141
  if channel_prefix is not None:
142
142
  kwargs["channel_prefix"] = channel_prefix
143
- self._backend = RedisEventBus(**kwargs)
143
+ self._backend = BroadcasterEventBus(**kwargs)
144
144
  elif backend == EventBackend.RABBITMQ:
145
+ if not url:
146
+ raise ValueError("RabbitMQ 后端需要提供 url 参数")
145
147
  self._backend = RabbitMQEventBus(url=url, exchange_name=exchange_name)
148
+ elif backend == EventBackend.ROCKETMQ:
149
+ raise NotImplementedError("RocketMQ 后端尚未实现")
146
150
  else:
147
151
  supported = ", ".join(b.value for b in EventBackend)
148
152
  raise ValueError(f"不支持的事件总线后端: {backend}。支持: {supported}")