aury-boot 0.0.30__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.
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +2 -4
- aury/boot/application/app/components.py +2 -0
- aury/boot/application/config/settings.py +6 -0
- aury/boot/commands/templates/project/AGENTS.md.tpl +54 -0
- aury/boot/commands/templates/project/env_templates/messaging.tpl +21 -13
- aury/boot/commands/templates/project/env_templates/monitoring.tpl +2 -0
- aury/boot/infrastructure/__init__.py +4 -8
- aury/boot/infrastructure/channel/__init__.py +9 -8
- aury/boot/infrastructure/channel/backends/__init__.py +2 -6
- aury/boot/infrastructure/channel/backends/broadcaster.py +141 -0
- aury/boot/infrastructure/channel/base.py +5 -2
- aury/boot/infrastructure/channel/manager.py +25 -24
- aury/boot/infrastructure/events/__init__.py +4 -6
- aury/boot/infrastructure/events/backends/__init__.py +2 -4
- aury/boot/infrastructure/events/backends/broadcaster.py +189 -0
- aury/boot/infrastructure/events/base.py +9 -4
- aury/boot/infrastructure/events/manager.py +24 -20
- aury/boot/infrastructure/monitoring/alerting/manager.py +2 -0
- aury/boot/infrastructure/monitoring/alerting/rules.py +16 -0
- aury/boot/infrastructure/monitoring/tracing/processor.py +31 -1
- aury/boot/infrastructure/monitoring/tracing/provider.py +2 -0
- {aury_boot-0.0.30.dist-info → aury_boot-0.0.31.dist-info}/METADATA +1 -1
- {aury_boot-0.0.30.dist-info → aury_boot-0.0.31.dist-info}/RECORD +26 -28
- aury/boot/infrastructure/channel/backends/memory.py +0 -126
- aury/boot/infrastructure/channel/backends/redis.py +0 -130
- aury/boot/infrastructure/events/backends/memory.py +0 -86
- aury/boot/infrastructure/events/backends/redis.py +0 -169
- {aury_boot-0.0.30.dist-info → aury_boot-0.0.31.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.30.dist-info → aury_boot-0.0.31.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
-
|
|
21
|
-
|
|
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.
|
|
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
|
-
- 多后端支持(
|
|
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="
|
|
40
|
-
|
|
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.
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 =
|
|
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}")
|
|
@@ -188,6 +188,7 @@ class AlertManager:
|
|
|
188
188
|
"""创建默认规则。"""
|
|
189
189
|
slow_request_threshold = self._defaults.get("slow_request_threshold", 1.0)
|
|
190
190
|
slow_sql_threshold = self._defaults.get("slow_sql_threshold", 0.5)
|
|
191
|
+
slow_request_exclude_paths = self._defaults.get("slow_request_exclude_paths") or None
|
|
191
192
|
|
|
192
193
|
default_rules = [
|
|
193
194
|
# 慢请求
|
|
@@ -198,6 +199,7 @@ class AlertManager:
|
|
|
198
199
|
aggregate_window=self._defaults.get("aggregate_window", 10),
|
|
199
200
|
aggregate_threshold=self._defaults.get("slow_request_aggregate", 5),
|
|
200
201
|
suppress_seconds=self._defaults.get("suppress_seconds", 300),
|
|
202
|
+
exclude_paths=slow_request_exclude_paths,
|
|
201
203
|
),
|
|
202
204
|
# 慢 SQL
|
|
203
205
|
AlertRule(
|
|
@@ -55,6 +55,7 @@ class AlertRule:
|
|
|
55
55
|
# 过滤条件
|
|
56
56
|
source_filter: str | None = None # api / task / scheduler
|
|
57
57
|
path_pattern: str | None = None # 路径匹配(支持 * 通配符)
|
|
58
|
+
exclude_paths: list[str] | None = None # 排除路径列表(支持 * 通配符)
|
|
58
59
|
|
|
59
60
|
# 聚合配置
|
|
60
61
|
aggregate_window: int = 10 # 滑动窗口(秒)
|
|
@@ -66,6 +67,7 @@ class AlertRule:
|
|
|
66
67
|
|
|
67
68
|
# 编译后的正则(内部使用)
|
|
68
69
|
_path_regex: re.Pattern | None = field(default=None, repr=False)
|
|
70
|
+
_exclude_regexes: list[re.Pattern] = field(default_factory=list, repr=False)
|
|
69
71
|
|
|
70
72
|
def __post_init__(self) -> None:
|
|
71
73
|
"""初始化后编译路径正则。"""
|
|
@@ -73,6 +75,12 @@ class AlertRule:
|
|
|
73
75
|
# 将通配符转换为正则
|
|
74
76
|
regex_pattern = fnmatch.translate(self.path_pattern)
|
|
75
77
|
self._path_regex = re.compile(regex_pattern)
|
|
78
|
+
|
|
79
|
+
if self.exclude_paths:
|
|
80
|
+
# 编译所有排除路径的正则
|
|
81
|
+
for exclude_pattern in self.exclude_paths:
|
|
82
|
+
regex_pattern = fnmatch.translate(exclude_pattern)
|
|
83
|
+
self._exclude_regexes.append(re.compile(regex_pattern))
|
|
76
84
|
|
|
77
85
|
def matches(self, event: "AlertEvent") -> bool:
|
|
78
86
|
"""检查事件是否匹配规则。
|
|
@@ -102,6 +110,13 @@ class AlertRule:
|
|
|
102
110
|
if not self._path_regex.match(endpoint):
|
|
103
111
|
return False
|
|
104
112
|
|
|
113
|
+
# 检查排除路径
|
|
114
|
+
if self._exclude_regexes:
|
|
115
|
+
endpoint = event.metadata.get("endpoint", "")
|
|
116
|
+
for exclude_regex in self._exclude_regexes:
|
|
117
|
+
if exclude_regex.match(endpoint):
|
|
118
|
+
return False # 匹配到排除规则,不触发告警
|
|
119
|
+
|
|
105
120
|
# 检查阈值(对于 slow_* 类型)
|
|
106
121
|
if self.threshold is not None and event.event_type in (
|
|
107
122
|
AlertEventType.SLOW_REQUEST,
|
|
@@ -147,6 +162,7 @@ def load_rules_from_dict(data: dict) -> tuple[dict, list[AlertRule]]:
|
|
|
147
162
|
severity_min=severity_min,
|
|
148
163
|
source_filter=rule_data.get("source_filter") or rule_data.get("source"),
|
|
149
164
|
path_pattern=rule_data.get("path_pattern"),
|
|
165
|
+
exclude_paths=rule_data.get("exclude_paths"),
|
|
150
166
|
aggregate_window=rule_data.get("aggregate_window", defaults.get("aggregate_window", 10)),
|
|
151
167
|
aggregate_threshold=rule_data.get("aggregate_threshold", defaults.get("aggregate_threshold", 1)),
|
|
152
168
|
suppress_seconds=rule_data.get("suppress_seconds", defaults.get("suppress_seconds", 300)),
|
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
from collections.abc import Awaitable, Callable
|
|
7
|
+
import fnmatch
|
|
8
|
+
import re
|
|
7
9
|
from typing import TYPE_CHECKING, Any
|
|
8
10
|
|
|
9
11
|
from aury.boot.common.logging import logger
|
|
@@ -49,6 +51,7 @@ class AlertingSpanProcessor:
|
|
|
49
51
|
alert_on_slow_sql: bool = True,
|
|
50
52
|
alert_on_error: bool = True,
|
|
51
53
|
alert_callback: AlertCallback | None = None,
|
|
54
|
+
slow_request_exclude_paths: list[str] | None = None,
|
|
52
55
|
) -> None:
|
|
53
56
|
"""初始化 AlertingSpanProcessor。
|
|
54
57
|
|
|
@@ -59,6 +62,7 @@ class AlertingSpanProcessor:
|
|
|
59
62
|
alert_on_slow_sql: 是否对慢 SQL 发送告警
|
|
60
63
|
alert_on_error: 是否对异常 span 发送告警
|
|
61
64
|
alert_callback: 告警回调函数,签名: async (event_type, message, **metadata) -> None
|
|
65
|
+
slow_request_exclude_paths: 慢请求排除路径列表(支持 * 通配符),如 SSE/WebSocket 长连接
|
|
62
66
|
"""
|
|
63
67
|
self._slow_request_threshold = slow_request_threshold
|
|
64
68
|
self._slow_sql_threshold = slow_sql_threshold
|
|
@@ -66,6 +70,13 @@ class AlertingSpanProcessor:
|
|
|
66
70
|
self._alert_on_slow_sql = alert_on_slow_sql
|
|
67
71
|
self._alert_on_error = alert_on_error
|
|
68
72
|
self._alert_callback = alert_callback
|
|
73
|
+
|
|
74
|
+
# 编译排除路径正则
|
|
75
|
+
self._exclude_regexes: list[re.Pattern] = []
|
|
76
|
+
if slow_request_exclude_paths:
|
|
77
|
+
for pattern in slow_request_exclude_paths:
|
|
78
|
+
regex_pattern = fnmatch.translate(pattern)
|
|
79
|
+
self._exclude_regexes.append(re.compile(regex_pattern))
|
|
69
80
|
|
|
70
81
|
def on_start(self, span: "Span", parent_context: object = None) -> None:
|
|
71
82
|
"""span 开始时调用(不做处理)。"""
|
|
@@ -95,6 +106,10 @@ class AlertingSpanProcessor:
|
|
|
95
106
|
|
|
96
107
|
# 检测慢 span
|
|
97
108
|
if should_alert and threshold > 0 and duration_s >= threshold:
|
|
109
|
+
# 检查是否在排除路径中
|
|
110
|
+
if self._is_path_excluded(name, attributes):
|
|
111
|
+
return
|
|
112
|
+
|
|
98
113
|
self._emit_slow_alert(
|
|
99
114
|
name=name,
|
|
100
115
|
duration=duration_s,
|
|
@@ -149,11 +164,25 @@ class AlertingSpanProcessor:
|
|
|
149
164
|
"""根据 span 类型判断是否应该发送慢告警。"""
|
|
150
165
|
if span_kind == "database":
|
|
151
166
|
return self._alert_on_slow_sql
|
|
152
|
-
elif span_kind in ("http", "http_client"):
|
|
167
|
+
elif span_kind in ("http", "http_client", "internal"):
|
|
153
168
|
return self._alert_on_slow_request
|
|
154
169
|
# 其他类型默认使用 HTTP 的开关
|
|
155
170
|
return self._alert_on_slow_request
|
|
156
171
|
|
|
172
|
+
def _is_path_excluded(self, name: str, attributes: dict) -> bool:
|
|
173
|
+
"""检查路径是否在排除列表中。"""
|
|
174
|
+
if not self._exclude_regexes:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
# 从 attributes 或 span name 中提取路径
|
|
178
|
+
path = (
|
|
179
|
+
attributes.get("http.route")
|
|
180
|
+
or attributes.get("http.target")
|
|
181
|
+
or name
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return any(regex.match(path) for regex in self._exclude_regexes)
|
|
185
|
+
|
|
157
186
|
def _emit_slow_alert(
|
|
158
187
|
self,
|
|
159
188
|
name: str,
|
|
@@ -260,6 +289,7 @@ def _get_event_type_for_slow(span_kind: str) -> str:
|
|
|
260
289
|
"http": "slow_request",
|
|
261
290
|
"database": "slow_sql",
|
|
262
291
|
"http_client": "slow_request",
|
|
292
|
+
"internal": "slow_request", # internal span 也用 slow_request 类型
|
|
263
293
|
}
|
|
264
294
|
return mapping.get(span_kind, "custom")
|
|
265
295
|
|
|
@@ -81,6 +81,7 @@ class TelemetryConfig:
|
|
|
81
81
|
alert_on_slow_sql: bool = True # 是否对慢 SQL 发送告警
|
|
82
82
|
alert_on_error: bool = True
|
|
83
83
|
alert_callback: Any = None # 告警回调函数
|
|
84
|
+
slow_request_exclude_paths: list[str] = field(default_factory=list) # 慢请求排除路径
|
|
84
85
|
|
|
85
86
|
# OTLP Traces 导出配置
|
|
86
87
|
traces_endpoint: str | None = None
|
|
@@ -181,6 +182,7 @@ class TelemetryProvider:
|
|
|
181
182
|
alert_on_slow_sql=self._config.alert_on_slow_sql,
|
|
182
183
|
alert_on_error=self._config.alert_on_error,
|
|
183
184
|
alert_callback=self._config.alert_callback,
|
|
185
|
+
slow_request_exclude_paths=self._config.slow_request_exclude_paths or None,
|
|
184
186
|
)
|
|
185
187
|
self._provider.add_span_processor(alerting_processor)
|
|
186
188
|
logger.debug("已添加 AlertingSpanProcessor")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
aury/boot/__init__.py,sha256=pCno-EInnpIBa1OtxNYF-JWf9j95Cd2h6vmu0xqa_-4,1791
|
|
2
|
-
aury/boot/_version.py,sha256=
|
|
3
|
-
aury/boot/application/__init__.py,sha256=
|
|
2
|
+
aury/boot/_version.py,sha256=nmSHmaFWTuD8SY4lecbyWq4JEPaihzn-zXy2K9lJGYo,706
|
|
3
|
+
aury/boot/application/__init__.py,sha256=I2KqNVdYg2q5nlOXr0TtFGyHmhj4oWdaR6ZB73Mwg7Y,3041
|
|
4
4
|
aury/boot/application/adapter/__init__.py,sha256=e1bcSb1bxUMfofTwiCuHBZJk5-STkMCWPF2EJXHQ7UU,3976
|
|
5
5
|
aury/boot/application/adapter/base.py,sha256=Ar_66fiHPDEmV-1DKnqXKwc53p3pozG31bgTJTEUriY,15763
|
|
6
6
|
aury/boot/application/adapter/config.py,sha256=X6ppQMldyJbEdG1GcQSc2SulLtyeBTr8OAboYIjkSu0,8153
|
|
@@ -9,12 +9,12 @@ aury/boot/application/adapter/exceptions.py,sha256=Kzm-ytRxdUnSMIcWCSOHPxo4Jh_A6
|
|
|
9
9
|
aury/boot/application/adapter/http.py,sha256=4TADsSzdSRU63307dmmo-2U_JpVP12mwTFy66B5Ps-w,10759
|
|
10
10
|
aury/boot/application/app/__init__.py,sha256=I8FfCKDuDQsGzAK6BevyfdtAwieMUVYu6qgVQzBazpE,830
|
|
11
11
|
aury/boot/application/app/base.py,sha256=7n7uFAVmIylr9YZannWKeQSOLSifNosIJUMbL-nmJe4,21897
|
|
12
|
-
aury/boot/application/app/components.py,sha256=
|
|
12
|
+
aury/boot/application/app/components.py,sha256=Ub7NlfxSPXSDcxUajQ5ed42kNmsBSol-UttcBfnx64Y,33473
|
|
13
13
|
aury/boot/application/app/middlewares.py,sha256=BXe2H14FHzJUVpQM6DZUm-zfZRXSXIi1QIZ4_3izfHw,3306
|
|
14
14
|
aury/boot/application/app/startup.py,sha256=DHKt3C2G7V5XfFr1SQMl14tNzcuDd9MqUVAxi274HDQ,7873
|
|
15
15
|
aury/boot/application/config/__init__.py,sha256=Dd-myRSBCM18DXXsi863h0cJG5VFrI10xMRtjnvelGo,1894
|
|
16
16
|
aury/boot/application/config/multi_instance.py,sha256=RXSp-xP8-bKMDEhq3SeL7T3lS8-vpRlvBEVBuZVjVK4,6475
|
|
17
|
-
aury/boot/application/config/settings.py,sha256=
|
|
17
|
+
aury/boot/application/config/settings.py,sha256=MPOjUyPxvWOrw878FOs0gnUPkqSUMyLpQ-MZz9yMwls,37866
|
|
18
18
|
aury/boot/application/constants/__init__.py,sha256=DCXs13_VVaQWHqO-qpJoZwRd7HIexiirtw_nu8msTXE,340
|
|
19
19
|
aury/boot/application/constants/components.py,sha256=I4SlsF2DpSzMiLsi1wVrEmdHn4yV5J2h3ikMQqufPmM,1120
|
|
20
20
|
aury/boot/application/constants/scheduler.py,sha256=S77FBIvHlyruvlabRWZJ2J1YAs2xWXPQI2yuGdGUDNA,471
|
|
@@ -61,7 +61,7 @@ aury/boot/commands/templates/generate/model.py.tpl,sha256=knFwMyGZ7wMpzH4_bQD_V1
|
|
|
61
61
|
aury/boot/commands/templates/generate/repository.py.tpl,sha256=xoEg6lPAaLIRDeFy4I0FBsPPVLSy91h6xosAlaCL_mM,590
|
|
62
62
|
aury/boot/commands/templates/generate/schema.py.tpl,sha256=HIaY5B0UG_S188nQLrZDEJ0q73WPdb7BmCdc0tseZA4,545
|
|
63
63
|
aury/boot/commands/templates/generate/service.py.tpl,sha256=2hwQ8e4a5d_bIMx_jGDobdmKPMFLBlfQrQVQH4Ym5k4,1842
|
|
64
|
-
aury/boot/commands/templates/project/AGENTS.md.tpl,sha256=
|
|
64
|
+
aury/boot/commands/templates/project/AGENTS.md.tpl,sha256=sp5qyzU-SGhgQCobpMW4EXRzpGsEsVdmJvspnKAP4AQ,10059
|
|
65
65
|
aury/boot/commands/templates/project/README.md.tpl,sha256=oCeBiukk6Pa3hrCKybkfM2sIRHsPZ15nlwuFTUSFDwY,2459
|
|
66
66
|
aury/boot/commands/templates/project/admin_console_init.py.tpl,sha256=K81L14thyEhRA8lFCQJVZL_NU22-sBz0xS68MJPeoCo,1541
|
|
67
67
|
aury/boot/commands/templates/project/alert_rules.example.yaml.tpl,sha256=QZH6SC5TcUhgX_2JRXk0k0g26wJf9xNwsdquiEIgg-I,2492
|
|
@@ -93,8 +93,8 @@ aury/boot/commands/templates/project/env_templates/admin.tpl,sha256=wWt3iybOpBHt
|
|
|
93
93
|
aury/boot/commands/templates/project/env_templates/cache.tpl,sha256=_sK-p_FECj4mVvggNvgb4Wu0yGii0Ocz560syG7DU2c,498
|
|
94
94
|
aury/boot/commands/templates/project/env_templates/database.tpl,sha256=2lWzTKt4X0SpeBBCkrDV90Di4EfoAuqYzhVsh74vTUI,907
|
|
95
95
|
aury/boot/commands/templates/project/env_templates/log.tpl,sha256=x5rkrEFJISH0gaCcr-wTCbDYtyFnlLNJpY789fqjZgc,754
|
|
96
|
-
aury/boot/commands/templates/project/env_templates/messaging.tpl,sha256=
|
|
97
|
-
aury/boot/commands/templates/project/env_templates/monitoring.tpl,sha256=
|
|
96
|
+
aury/boot/commands/templates/project/env_templates/messaging.tpl,sha256=SzPRKwN0wO5e1kpjkSwpPJfVmiUDzZkK4Qm-qNsCvVE,2178
|
|
97
|
+
aury/boot/commands/templates/project/env_templates/monitoring.tpl,sha256=Zq0xQzDrCRtbeLCQB3pkEE2p8FFED6IjQo4TqMyd_P8,2584
|
|
98
98
|
aury/boot/commands/templates/project/env_templates/rpc.tpl,sha256=FhweCFakawGLSs01a_BkmZo11UhWax2-VCBudHj68WA,1163
|
|
99
99
|
aury/boot/commands/templates/project/env_templates/scheduler.tpl,sha256=c8Grcs1rgBB58RHlxqmDMPHQl8BnbcqNW473ctmsojU,752
|
|
100
100
|
aury/boot/commands/templates/project/env_templates/service.tpl,sha256=b-a2GyRyoaunbDj_2kaSw3OFxcugscmPvUBG7w0XO8c,710
|
|
@@ -134,19 +134,18 @@ aury/boot/domain/repository/query_builder.py,sha256=pFErMzsBql-T6gBX0S4FxIheCkNa
|
|
|
134
134
|
aury/boot/domain/service/__init__.py,sha256=ZRotaBlqJXn7ebPTQjjoHtorpQREk8AgTD69UCcRd1k,118
|
|
135
135
|
aury/boot/domain/service/base.py,sha256=6sN0nf8r5yUZsE6AcZOiOXFCqzb61oCxTfrWlqjIo9I,2035
|
|
136
136
|
aury/boot/domain/transaction/__init__.py,sha256=EKnjJ235SYjMCvGIuLVlTdYRzU35RxNMejRGUExYqqE,15488
|
|
137
|
-
aury/boot/infrastructure/__init__.py,sha256=
|
|
137
|
+
aury/boot/infrastructure/__init__.py,sha256=DDEr_BIL5OyMJjNlI05jGIUrSHn6MPdnW9xnCS4eHfg,2949
|
|
138
138
|
aury/boot/infrastructure/cache/__init__.py,sha256=G40uCkpJ1jSs2fc_CBDem73iQQzCcp-4GG1WpDJzwaA,658
|
|
139
139
|
aury/boot/infrastructure/cache/backends.py,sha256=9QMQ8G9DtZgzVXZ_Ng7n1gXRu-_OQZgw4FHPOfr1qco,13585
|
|
140
140
|
aury/boot/infrastructure/cache/base.py,sha256=Yn-h_SGcOoGGZW1unOnz_zgcuHaMKOEmwiUP0P7_pIM,1624
|
|
141
141
|
aury/boot/infrastructure/cache/exceptions.py,sha256=KZsFIHXW3_kOh_KB93EVZJKbiDvDw8aloAefJ3kasP8,622
|
|
142
142
|
aury/boot/infrastructure/cache/factory.py,sha256=aF74JoiiSKFgctqqh2Z8OtGRS2Am_ou-I40GyygLzC0,2489
|
|
143
143
|
aury/boot/infrastructure/cache/manager.py,sha256=GGoOgYyIdWKMmhej5cRvEfpNeMN1GaSaU9hc0dy8_sA,12106
|
|
144
|
-
aury/boot/infrastructure/channel/__init__.py,sha256=
|
|
145
|
-
aury/boot/infrastructure/channel/base.py,sha256=
|
|
146
|
-
aury/boot/infrastructure/channel/manager.py,sha256=
|
|
147
|
-
aury/boot/infrastructure/channel/backends/__init__.py,sha256=
|
|
148
|
-
aury/boot/infrastructure/channel/backends/
|
|
149
|
-
aury/boot/infrastructure/channel/backends/redis.py,sha256=_UL7wE-bO147CPXKDjJgYGjj09Lg9x9U2PLYa37q5yQ,4666
|
|
144
|
+
aury/boot/infrastructure/channel/__init__.py,sha256=NmjddenZPz1Dcl0glwIF1Xn9gxBzvGvlOlzhV3eEnEQ,664
|
|
145
|
+
aury/boot/infrastructure/channel/base.py,sha256=TDiP7pXyd2ixiOM3cbxqCSOluGLTkmLCa8pv-KyQ0jo,2941
|
|
146
|
+
aury/boot/infrastructure/channel/manager.py,sha256=GT6eG6PglduKAr23i1PSmjjTQsALvGGoLjYiQ33aZiw,7488
|
|
147
|
+
aury/boot/infrastructure/channel/backends/__init__.py,sha256=NcXG8_KAqy1SiGUs2z_KvkS90jMfLJ6bzyYK4Jw4qCg,107
|
|
148
|
+
aury/boot/infrastructure/channel/backends/broadcaster.py,sha256=y8eKx6X6Iy9a_5vnLMm5gjqkq05SmJEWESw1-x0lIFg,4771
|
|
150
149
|
aury/boot/infrastructure/clients/__init__.py,sha256=1ANMejb3RrBgaR-jq-dsxJ0kQDRHz5jV-QvdUNcf_ok,435
|
|
151
150
|
aury/boot/infrastructure/clients/rabbitmq/__init__.py,sha256=cnU-W7jOcAgp_FvsY9EipNCeJzeA9gHLRuZ0yQZE2DI,200
|
|
152
151
|
aury/boot/infrastructure/clients/rabbitmq/config.py,sha256=YmvNiISpqNt-LE2CrpzmxCgaEgYna7IbOfUSnA0B4T0,1239
|
|
@@ -162,20 +161,19 @@ aury/boot/infrastructure/database/query_tools/__init__.py,sha256=pOFuyDDNpkY5cSM
|
|
|
162
161
|
aury/boot/infrastructure/database/strategies/__init__.py,sha256=foj_2xEsgLZxshpK65YAhdJ2UZyh1tKvGRq6sre8pQY,5909
|
|
163
162
|
aury/boot/infrastructure/di/__init__.py,sha256=qFYlk265d6_rS8OiX37_wOc7mBFw8hk3yipDYNkyjQg,231
|
|
164
163
|
aury/boot/infrastructure/di/container.py,sha256=14FVbafGXea-JEAYeOEBxB6zAwndLCZJvprKiD_1IOQ,12524
|
|
165
|
-
aury/boot/infrastructure/events/__init__.py,sha256
|
|
166
|
-
aury/boot/infrastructure/events/base.py,sha256=
|
|
167
|
-
aury/boot/infrastructure/events/manager.py,sha256=
|
|
164
|
+
aury/boot/infrastructure/events/__init__.py,sha256=-fXZiTKkwh2olyw8BCGO-Qv67ZDCS0enioY005vPhrI,676
|
|
165
|
+
aury/boot/infrastructure/events/base.py,sha256=m5sXe7rpdvq6pcgCAn2PX7lLXZgCpujteRqHXQ0oaqs,3238
|
|
166
|
+
aury/boot/infrastructure/events/manager.py,sha256=aQNzyPuz1oWAMkXz4UQqCRSPiGTcm8HmSE1lGWovhWE,7754
|
|
168
167
|
aury/boot/infrastructure/events/middleware.py,sha256=Ck3qNMTtLuFFKsJuEUeOMG9nu3qK1N_aqt6wH5JoAtw,1336
|
|
169
|
-
aury/boot/infrastructure/events/backends/__init__.py,sha256=
|
|
170
|
-
aury/boot/infrastructure/events/backends/
|
|
168
|
+
aury/boot/infrastructure/events/backends/__init__.py,sha256=1mj0rDauHdoRm4kXOg87l2f9jnMbj_jKZdVnIZMj9XM,185
|
|
169
|
+
aury/boot/infrastructure/events/backends/broadcaster.py,sha256=FnxO62LUXWLs1ZEiaYmNiMaL3ccXNtuc3DFzLe02eK0,6700
|
|
171
170
|
aury/boot/infrastructure/events/backends/rabbitmq.py,sha256=XCuI9mc3GR-t0zht4yZ3e2nnyFl8UuTDir_0nsDbfxM,6495
|
|
172
|
-
aury/boot/infrastructure/events/backends/redis.py,sha256=i8jPCtR7ITPVTl9DVFDbNbjypnWoeSpar6z4lJJlOD8,5790
|
|
173
171
|
aury/boot/infrastructure/monitoring/__init__.py,sha256=KGtJU0slbRvFzzUv60LQHB12sX7eNNvGDu8Lyk9Owy8,22415
|
|
174
172
|
aury/boot/infrastructure/monitoring/alerting/__init__.py,sha256=gBZ23JnCjqglyYTTUxfkmilZ4mY_ZkrkKMDo--3COGE,1363
|
|
175
173
|
aury/boot/infrastructure/monitoring/alerting/aggregator.py,sha256=fiI-lBSqWxXv1eVPfaDNjcigX-81w41fcmhD_vN_XSs,5805
|
|
176
174
|
aury/boot/infrastructure/monitoring/alerting/events.py,sha256=zJvTevQ-9JflIDyYVo1BRzOVyAGhdgEfRlMsD0NcBgM,4056
|
|
177
|
-
aury/boot/infrastructure/monitoring/alerting/manager.py,sha256=
|
|
178
|
-
aury/boot/infrastructure/monitoring/alerting/rules.py,sha256=
|
|
175
|
+
aury/boot/infrastructure/monitoring/alerting/manager.py,sha256=ZcIMhBgHmrSGn8FeJOIBIiwKOcdWvT6NBm-6wvRmGc4,14525
|
|
176
|
+
aury/boot/infrastructure/monitoring/alerting/rules.py,sha256=XcXJXWVrPpdZKKz63BiVWmwkKitIaNQWBfJATrSzG1M,6116
|
|
179
177
|
aury/boot/infrastructure/monitoring/alerting/notifiers/__init__.py,sha256=dsfxThPHO_Ofb3Wo_dYlL8HvP_N63pb_S_UXm_qSxF8,321
|
|
180
178
|
aury/boot/infrastructure/monitoring/alerting/notifiers/base.py,sha256=_RXZMzWX-YeTG0Up1U8CwK8ADfX34dd0Sh56ugfqOWM,1462
|
|
181
179
|
aury/boot/infrastructure/monitoring/alerting/notifiers/feishu.py,sha256=JAMJiCNRYoDeJrYn29ew_ZVXDGq8OLgiFApRWd4iPY0,7134
|
|
@@ -184,8 +182,8 @@ aury/boot/infrastructure/monitoring/health/__init__.py,sha256=nqwFFXl6J9yTfQa1JL
|
|
|
184
182
|
aury/boot/infrastructure/monitoring/tracing/__init__.py,sha256=YizkpnhY-bcUUcd8YaDzUsluMflhNOH1dAKdVtkW05U,1287
|
|
185
183
|
aury/boot/infrastructure/monitoring/tracing/context.py,sha256=s_k2MzNl4LDDpei9xUP6TFW5BwZneoQg44RPaw95jac,978
|
|
186
184
|
aury/boot/infrastructure/monitoring/tracing/logging.py,sha256=gzuKa1ZiyY4z06fHNTbjgZasS6mLftSEaZQQ-Z6J_RE,2041
|
|
187
|
-
aury/boot/infrastructure/monitoring/tracing/processor.py,sha256=
|
|
188
|
-
aury/boot/infrastructure/monitoring/tracing/provider.py,sha256=
|
|
185
|
+
aury/boot/infrastructure/monitoring/tracing/processor.py,sha256=qc37YmS8rslpwqAYHrBDzVvNWmXRIFEwpld34NMmByk,12640
|
|
186
|
+
aury/boot/infrastructure/monitoring/tracing/provider.py,sha256=AnPHUDHnfrCB48WHjp9vLBhCh9BpyfWb3DHGRh6Din4,11553
|
|
189
187
|
aury/boot/infrastructure/monitoring/tracing/tracing.py,sha256=BeWL-FYtlQ05r05wGJ6qjTSpypgCp-7OzdNnZ3uunB0,6890
|
|
190
188
|
aury/boot/infrastructure/mq/__init__.py,sha256=Q7kBk_GeQnxnqkyp29Bh1yFH3Q8xxxjs8oDYLeDj8C0,498
|
|
191
189
|
aury/boot/infrastructure/mq/base.py,sha256=kHrWUysWflMj3qyOnioLZ90it8d9Alq1Wb4PYhpBW4k,3396
|
|
@@ -211,7 +209,7 @@ aury/boot/testing/client.py,sha256=KOg1EemuIVsBG68G5y0DjSxZGcIQVdWQ4ASaHE3o1R0,4
|
|
|
211
209
|
aury/boot/testing/factory.py,sha256=8GvwX9qIDu0L65gzJMlrWB0xbmJ-7zPHuwk3eECULcg,5185
|
|
212
210
|
aury/boot/toolkit/__init__.py,sha256=AcyVb9fDf3CaEmJPNkWC4iGv32qCPyk4BuFKSuNiJRQ,334
|
|
213
211
|
aury/boot/toolkit/http/__init__.py,sha256=zIPmpIZ9Qbqe25VmEr7jixoY2fkRbLm7NkCB9vKpg6I,11039
|
|
214
|
-
aury_boot-0.0.
|
|
215
|
-
aury_boot-0.0.
|
|
216
|
-
aury_boot-0.0.
|
|
217
|
-
aury_boot-0.0.
|
|
212
|
+
aury_boot-0.0.31.dist-info/METADATA,sha256=UPuRX_9BPO-OPlk78XK1ynbzHq-je4mrKTKrAvsouzA,8560
|
|
213
|
+
aury_boot-0.0.31.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
214
|
+
aury_boot-0.0.31.dist-info/entry_points.txt,sha256=f9KXEkDIGc0BGkgBvsNx_HMz9VhDjNxu26q00jUpDwQ,49
|
|
215
|
+
aury_boot-0.0.31.dist-info/RECORD,,
|