brawny 0.1.13__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.
- brawny/__init__.py +106 -0
- brawny/_context.py +232 -0
- brawny/_rpc/__init__.py +38 -0
- brawny/_rpc/broadcast.py +172 -0
- brawny/_rpc/clients.py +98 -0
- brawny/_rpc/context.py +49 -0
- brawny/_rpc/errors.py +252 -0
- brawny/_rpc/gas.py +158 -0
- brawny/_rpc/manager.py +982 -0
- brawny/_rpc/selector.py +156 -0
- brawny/accounts.py +534 -0
- brawny/alerts/__init__.py +132 -0
- brawny/alerts/abi_resolver.py +530 -0
- brawny/alerts/base.py +152 -0
- brawny/alerts/context.py +271 -0
- brawny/alerts/contracts.py +635 -0
- brawny/alerts/encoded_call.py +201 -0
- brawny/alerts/errors.py +267 -0
- brawny/alerts/events.py +680 -0
- brawny/alerts/function_caller.py +364 -0
- brawny/alerts/health.py +185 -0
- brawny/alerts/routing.py +118 -0
- brawny/alerts/send.py +364 -0
- brawny/api.py +660 -0
- brawny/chain.py +93 -0
- brawny/cli/__init__.py +16 -0
- brawny/cli/app.py +17 -0
- brawny/cli/bootstrap.py +37 -0
- brawny/cli/commands/__init__.py +41 -0
- brawny/cli/commands/abi.py +93 -0
- brawny/cli/commands/accounts.py +632 -0
- brawny/cli/commands/console.py +495 -0
- brawny/cli/commands/contract.py +139 -0
- brawny/cli/commands/health.py +112 -0
- brawny/cli/commands/init_project.py +86 -0
- brawny/cli/commands/intents.py +130 -0
- brawny/cli/commands/job_dev.py +254 -0
- brawny/cli/commands/jobs.py +308 -0
- brawny/cli/commands/logs.py +87 -0
- brawny/cli/commands/maintenance.py +182 -0
- brawny/cli/commands/migrate.py +51 -0
- brawny/cli/commands/networks.py +253 -0
- brawny/cli/commands/run.py +249 -0
- brawny/cli/commands/script.py +209 -0
- brawny/cli/commands/signer.py +248 -0
- brawny/cli/helpers.py +265 -0
- brawny/cli_templates.py +1445 -0
- brawny/config/__init__.py +74 -0
- brawny/config/models.py +404 -0
- brawny/config/parser.py +633 -0
- brawny/config/routing.py +55 -0
- brawny/config/validation.py +246 -0
- brawny/daemon/__init__.py +14 -0
- brawny/daemon/context.py +69 -0
- brawny/daemon/core.py +702 -0
- brawny/daemon/loops.py +327 -0
- brawny/db/__init__.py +78 -0
- brawny/db/base.py +986 -0
- brawny/db/base_new.py +165 -0
- brawny/db/circuit_breaker.py +97 -0
- brawny/db/global_cache.py +298 -0
- brawny/db/mappers.py +182 -0
- brawny/db/migrate.py +349 -0
- brawny/db/migrations/001_init.sql +186 -0
- brawny/db/migrations/002_add_included_block.sql +7 -0
- brawny/db/migrations/003_add_broadcast_at.sql +10 -0
- brawny/db/migrations/004_broadcast_binding.sql +20 -0
- brawny/db/migrations/005_add_retry_after.sql +9 -0
- brawny/db/migrations/006_add_retry_count_column.sql +11 -0
- brawny/db/migrations/007_add_gap_tracking.sql +18 -0
- brawny/db/migrations/008_add_transactions.sql +72 -0
- brawny/db/migrations/009_add_intent_metadata.sql +5 -0
- brawny/db/migrations/010_add_nonce_gap_index.sql +9 -0
- brawny/db/migrations/011_add_job_logs.sql +24 -0
- brawny/db/migrations/012_add_claimed_by.sql +5 -0
- brawny/db/ops/__init__.py +29 -0
- brawny/db/ops/attempts.py +108 -0
- brawny/db/ops/blocks.py +83 -0
- brawny/db/ops/cache.py +93 -0
- brawny/db/ops/intents.py +296 -0
- brawny/db/ops/jobs.py +110 -0
- brawny/db/ops/logs.py +97 -0
- brawny/db/ops/nonces.py +322 -0
- brawny/db/postgres.py +2535 -0
- brawny/db/postgres_new.py +196 -0
- brawny/db/queries.py +584 -0
- brawny/db/sqlite.py +2733 -0
- brawny/db/sqlite_new.py +191 -0
- brawny/history.py +126 -0
- brawny/interfaces.py +136 -0
- brawny/invariants.py +155 -0
- brawny/jobs/__init__.py +26 -0
- brawny/jobs/base.py +287 -0
- brawny/jobs/discovery.py +233 -0
- brawny/jobs/job_validation.py +111 -0
- brawny/jobs/kv.py +125 -0
- brawny/jobs/registry.py +283 -0
- brawny/keystore.py +484 -0
- brawny/lifecycle.py +551 -0
- brawny/logging.py +290 -0
- brawny/metrics.py +594 -0
- brawny/model/__init__.py +53 -0
- brawny/model/contexts.py +319 -0
- brawny/model/enums.py +70 -0
- brawny/model/errors.py +194 -0
- brawny/model/events.py +93 -0
- brawny/model/startup.py +20 -0
- brawny/model/types.py +483 -0
- brawny/networks/__init__.py +96 -0
- brawny/networks/config.py +269 -0
- brawny/networks/manager.py +423 -0
- brawny/obs/__init__.py +67 -0
- brawny/obs/emit.py +158 -0
- brawny/obs/health.py +175 -0
- brawny/obs/heartbeat.py +133 -0
- brawny/reconciliation.py +108 -0
- brawny/scheduler/__init__.py +19 -0
- brawny/scheduler/poller.py +472 -0
- brawny/scheduler/reorg.py +632 -0
- brawny/scheduler/runner.py +708 -0
- brawny/scheduler/shutdown.py +371 -0
- brawny/script_tx.py +297 -0
- brawny/scripting.py +251 -0
- brawny/startup.py +76 -0
- brawny/telegram.py +393 -0
- brawny/testing.py +108 -0
- brawny/tx/__init__.py +41 -0
- brawny/tx/executor.py +1071 -0
- brawny/tx/fees.py +50 -0
- brawny/tx/intent.py +423 -0
- brawny/tx/monitor.py +628 -0
- brawny/tx/nonce.py +498 -0
- brawny/tx/replacement.py +456 -0
- brawny/tx/utils.py +26 -0
- brawny/utils.py +205 -0
- brawny/validation.py +69 -0
- brawny-0.1.13.dist-info/METADATA +156 -0
- brawny-0.1.13.dist-info/RECORD +141 -0
- brawny-0.1.13.dist-info/WHEEL +5 -0
- brawny-0.1.13.dist-info/entry_points.txt +2 -0
- brawny-0.1.13.dist-info/top_level.txt +1 -0
brawny/alerts/send.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""Simplified alert system.
|
|
2
|
+
|
|
3
|
+
Send alerts to Telegram or webhooks. No classes. No inheritance. No plugin architecture.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
payload = AlertPayload(
|
|
7
|
+
job_id="my-job",
|
|
8
|
+
job_name="My Job",
|
|
9
|
+
event_type=AlertEvent.CONFIRMED,
|
|
10
|
+
message="Transaction confirmed!",
|
|
11
|
+
)
|
|
12
|
+
config = AlertConfig(
|
|
13
|
+
telegram_token="...",
|
|
14
|
+
telegram_chat_ids=["123456"],
|
|
15
|
+
)
|
|
16
|
+
await send_alert(payload, config)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import threading
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from enum import Enum
|
|
26
|
+
from typing import TYPE_CHECKING, Any, Callable, Coroutine
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from brawny.telegram import TelegramBot
|
|
30
|
+
|
|
31
|
+
import httpx
|
|
32
|
+
|
|
33
|
+
from brawny.logging import get_logger
|
|
34
|
+
|
|
35
|
+
logger = get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AlertEvent(str, Enum):
|
|
39
|
+
"""Alert event types. Aligned with OE2 hook reduction."""
|
|
40
|
+
|
|
41
|
+
TRIGGERED = "triggered"
|
|
42
|
+
CONFIRMED = "confirmed"
|
|
43
|
+
FAILED = "failed"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class AlertPayload:
|
|
48
|
+
"""Data object for alert content."""
|
|
49
|
+
|
|
50
|
+
job_id: str
|
|
51
|
+
job_name: str
|
|
52
|
+
event_type: AlertEvent
|
|
53
|
+
message: str
|
|
54
|
+
parse_mode: str = "Markdown"
|
|
55
|
+
chain_id: int = 1
|
|
56
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class AlertConfig:
|
|
61
|
+
"""Transport configuration. Passed once, not spread across callsites."""
|
|
62
|
+
|
|
63
|
+
telegram_token: str | None = None
|
|
64
|
+
telegram_chat_ids: list[str] = field(default_factory=list)
|
|
65
|
+
webhook_url: str | None = None
|
|
66
|
+
rate_limit_seconds: float = 3.0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Module-level state for rate limiting only
|
|
70
|
+
# NOTE: No module-level httpx.AsyncClient - asyncio objects are not safe to share
|
|
71
|
+
# across multiple event loops / loop lifetimes. For low-volume alerts, we create
|
|
72
|
+
# a fresh client per request (httpx context manager handles cleanup).
|
|
73
|
+
_last_sent: dict[str, datetime] = {}
|
|
74
|
+
# Use threading.Lock, not asyncio.Lock - avoids event loop binding issues
|
|
75
|
+
_last_sent_lock = threading.Lock()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def send_alert(payload: AlertPayload, config: AlertConfig) -> None:
|
|
79
|
+
"""Send alert to configured destinations. Fire-and-forget."""
|
|
80
|
+
tasks: list[Coroutine[Any, Any, None]] = []
|
|
81
|
+
|
|
82
|
+
if config.telegram_token and config.telegram_chat_ids:
|
|
83
|
+
for chat_id in config.telegram_chat_ids:
|
|
84
|
+
if _should_send(payload, "telegram", chat_id, config.rate_limit_seconds):
|
|
85
|
+
tasks.append(_send_telegram(config.telegram_token, chat_id, payload))
|
|
86
|
+
|
|
87
|
+
if config.webhook_url:
|
|
88
|
+
if _should_send(payload, "webhook", config.webhook_url, config.rate_limit_seconds):
|
|
89
|
+
tasks.append(_send_webhook(config.webhook_url, payload))
|
|
90
|
+
|
|
91
|
+
if tasks:
|
|
92
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
93
|
+
for i, result in enumerate(results):
|
|
94
|
+
if isinstance(result, Exception):
|
|
95
|
+
_log_failure(payload, tasks[i], result)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _should_send(
|
|
99
|
+
payload: AlertPayload,
|
|
100
|
+
dest_type: str,
|
|
101
|
+
destination_id: str,
|
|
102
|
+
limit_seconds: float,
|
|
103
|
+
) -> bool:
|
|
104
|
+
"""Check rate limit. Key includes dest_type to avoid collisions.
|
|
105
|
+
|
|
106
|
+
Key format: job_id:event_type:dest_type:destination_id
|
|
107
|
+
- Multiple chat IDs rate-limited independently
|
|
108
|
+
- Telegram + webhook don't suppress each other
|
|
109
|
+
- dest_type prevents test collisions
|
|
110
|
+
|
|
111
|
+
Uses threading.Lock (not asyncio.Lock) to avoid event loop binding issues.
|
|
112
|
+
"""
|
|
113
|
+
key = f"{payload.job_id}:{payload.event_type.value}:{dest_type}:{destination_id}"
|
|
114
|
+
|
|
115
|
+
with _last_sent_lock:
|
|
116
|
+
now = datetime.utcnow()
|
|
117
|
+
if key in _last_sent:
|
|
118
|
+
if (now - _last_sent[key]).total_seconds() < limit_seconds:
|
|
119
|
+
return False
|
|
120
|
+
_last_sent[key] = now
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def _send_telegram(token: str, chat_id: str, payload: AlertPayload) -> None:
|
|
125
|
+
"""Send message to Telegram. Pure function, no state."""
|
|
126
|
+
parse_mode = payload.parse_mode or "Markdown"
|
|
127
|
+
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
|
128
|
+
data = {
|
|
129
|
+
"chat_id": chat_id,
|
|
130
|
+
"text": payload.message,
|
|
131
|
+
"parse_mode": parse_mode,
|
|
132
|
+
"disable_web_page_preview": True,
|
|
133
|
+
}
|
|
134
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
135
|
+
resp = await client.post(url, json=data)
|
|
136
|
+
resp.raise_for_status()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def _send_webhook(url: str, payload: AlertPayload) -> None:
|
|
140
|
+
"""Send payload to webhook. Pure function, no state.
|
|
141
|
+
|
|
142
|
+
Schema (frozen):
|
|
143
|
+
- job_id: str
|
|
144
|
+
- job_name: str
|
|
145
|
+
- event_type: str (enum value)
|
|
146
|
+
- message: str
|
|
147
|
+
- chain_id: int
|
|
148
|
+
- timestamp: str (ISO8601 UTC)
|
|
149
|
+
|
|
150
|
+
Do not add fields without versioning discussion.
|
|
151
|
+
"""
|
|
152
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
153
|
+
resp = await client.post(
|
|
154
|
+
url,
|
|
155
|
+
json={
|
|
156
|
+
"job_id": payload.job_id,
|
|
157
|
+
"job_name": payload.job_name,
|
|
158
|
+
"event_type": payload.event_type.value,
|
|
159
|
+
"message": payload.message,
|
|
160
|
+
"chain_id": payload.chain_id,
|
|
161
|
+
"timestamp": payload.timestamp.isoformat() + "Z",
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
resp.raise_for_status()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _log_failure(payload: AlertPayload, task: Coroutine[Any, Any, None], error: Exception) -> None:
|
|
168
|
+
"""Log alert failure with enough context to debug."""
|
|
169
|
+
task_name = task.__qualname__ if hasattr(task, "__qualname__") else str(task)
|
|
170
|
+
|
|
171
|
+
if "telegram" in task_name.lower():
|
|
172
|
+
logger.warning(
|
|
173
|
+
"alert_delivery_failed",
|
|
174
|
+
job_id=payload.job_id,
|
|
175
|
+
event_type=payload.event_type.value,
|
|
176
|
+
destination="telegram",
|
|
177
|
+
error=str(error),
|
|
178
|
+
)
|
|
179
|
+
elif "webhook" in task_name.lower():
|
|
180
|
+
logger.warning(
|
|
181
|
+
"alert_delivery_failed",
|
|
182
|
+
job_id=payload.job_id,
|
|
183
|
+
event_type=payload.event_type.value,
|
|
184
|
+
destination="webhook",
|
|
185
|
+
error=str(error),
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
logger.warning(
|
|
189
|
+
"alert_delivery_failed",
|
|
190
|
+
job_id=payload.job_id,
|
|
191
|
+
event_type=payload.event_type.value,
|
|
192
|
+
error=str(error),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# =============================================================================
|
|
197
|
+
# Public alert() Function for Job Hooks
|
|
198
|
+
# =============================================================================
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def alert(
|
|
202
|
+
message: str,
|
|
203
|
+
*,
|
|
204
|
+
to: str | list[str] | None = None,
|
|
205
|
+
parse_mode: str | None = None,
|
|
206
|
+
disable_web_page_preview: bool = True,
|
|
207
|
+
disable_notification: bool = False,
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Send alert from within a job hook.
|
|
210
|
+
|
|
211
|
+
Handles routing resolution, then delegates to TelegramBot.send_message().
|
|
212
|
+
|
|
213
|
+
Uses Telegram Bot API parameter names verbatim. No aliases or renaming.
|
|
214
|
+
Refer to https://core.telegram.org/bots/api#sendmessage for parameter docs.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
message: Alert text (up to 4096 characters, auto-truncated)
|
|
218
|
+
to: Override routing target (name, raw ID, or list). If None,
|
|
219
|
+
uses job's alert_to config, then global default.
|
|
220
|
+
Note: This is a routing concept, not a Telegram API field.
|
|
221
|
+
parse_mode: "Markdown", "MarkdownV2", "HTML", or None
|
|
222
|
+
disable_web_page_preview: Disable link previews (default True)
|
|
223
|
+
disable_notification: Send without notification sound (default False)
|
|
224
|
+
|
|
225
|
+
Resolution order:
|
|
226
|
+
1. `to` parameter (if provided)
|
|
227
|
+
2. Job's alert_to config (if set)
|
|
228
|
+
3. Global default (from config)
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
RuntimeError: If called outside a job hook
|
|
232
|
+
|
|
233
|
+
Note:
|
|
234
|
+
Unknown chat names are logged and skipped (not raised). Startup
|
|
235
|
+
validation catches typos during normal deployment; runtime errors
|
|
236
|
+
are logged but don't crash hooks.
|
|
237
|
+
|
|
238
|
+
Example:
|
|
239
|
+
alert("Harvested successfully")
|
|
240
|
+
alert("Debug info", to="dev", disable_notification=True)
|
|
241
|
+
alert("Check https://etherscan.io/tx/...", disable_web_page_preview=False)
|
|
242
|
+
"""
|
|
243
|
+
from brawny._context import get_alert_context
|
|
244
|
+
from brawny.alerts.routing import resolve_targets
|
|
245
|
+
|
|
246
|
+
ctx = get_alert_context()
|
|
247
|
+
if ctx is None:
|
|
248
|
+
raise RuntimeError("alert() must be called from within a job hook")
|
|
249
|
+
|
|
250
|
+
# Get telegram config and bot from context
|
|
251
|
+
tg_config = getattr(ctx, "telegram_config", None)
|
|
252
|
+
bot = getattr(ctx, "telegram_bot", None)
|
|
253
|
+
if not tg_config or not bot:
|
|
254
|
+
return # Silent no-op (warned once at startup)
|
|
255
|
+
|
|
256
|
+
# Determine target
|
|
257
|
+
if to is not None:
|
|
258
|
+
target = to
|
|
259
|
+
else:
|
|
260
|
+
job_alert_to = getattr(ctx, "job_alert_to", None)
|
|
261
|
+
target = job_alert_to if job_alert_to is not None else None
|
|
262
|
+
|
|
263
|
+
# Resolve to chat IDs (unknown names logged + skipped, not raised)
|
|
264
|
+
job_id = getattr(ctx, "job_id", None)
|
|
265
|
+
chat_ids = resolve_targets(
|
|
266
|
+
target,
|
|
267
|
+
tg_config.chats,
|
|
268
|
+
tg_config.default,
|
|
269
|
+
job_id=job_id,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if not chat_ids:
|
|
273
|
+
return # No targets configured (or all were invalid)
|
|
274
|
+
|
|
275
|
+
# Send to each resolved chat
|
|
276
|
+
for chat_id in chat_ids:
|
|
277
|
+
effective_parse_mode = (
|
|
278
|
+
parse_mode if parse_mode is not None else tg_config.parse_mode or "Markdown"
|
|
279
|
+
)
|
|
280
|
+
bot.send_message(
|
|
281
|
+
message,
|
|
282
|
+
chat_id=chat_id,
|
|
283
|
+
parse_mode=effective_parse_mode,
|
|
284
|
+
disable_web_page_preview=disable_web_page_preview,
|
|
285
|
+
disable_notification=disable_notification,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def _send_alert_logged(payload: AlertPayload, config: AlertConfig) -> None:
|
|
290
|
+
"""Fire-and-forget alert with exception logging."""
|
|
291
|
+
try:
|
|
292
|
+
await send_alert(payload, config)
|
|
293
|
+
except Exception:
|
|
294
|
+
logger.exception("Failed to send alert", job_id=payload.job_id)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# =============================================================================
|
|
298
|
+
# Health Alert Sender (distinct rate limiting from job alerts)
|
|
299
|
+
# =============================================================================
|
|
300
|
+
|
|
301
|
+
# Separate rate limiting for health alerts (prevents job alert noise from blocking health)
|
|
302
|
+
_health_last_sent: dict[str, datetime] = {}
|
|
303
|
+
_health_lock = threading.Lock()
|
|
304
|
+
|
|
305
|
+
HEALTH_RATE_LIMIT_SECONDS = 1.0 # Min interval between health messages to same chat
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _should_send_health(chat_id: str) -> bool:
|
|
309
|
+
"""Check rate limit for health alerts. Uses separate namespace from job alerts."""
|
|
310
|
+
key = f"health:{chat_id}"
|
|
311
|
+
with _health_lock:
|
|
312
|
+
now = datetime.utcnow()
|
|
313
|
+
if key in _health_last_sent:
|
|
314
|
+
if (now - _health_last_sent[key]).total_seconds() < HEALTH_RATE_LIMIT_SECONDS:
|
|
315
|
+
return False
|
|
316
|
+
_health_last_sent[key] = now
|
|
317
|
+
return True
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def create_send_health(bot: "TelegramBot") -> "Callable[[str, str], None]":
|
|
321
|
+
"""Create a health alert sender bound to a TelegramBot instance.
|
|
322
|
+
|
|
323
|
+
Returns a callable that accepts (chat_id, text) kwargs.
|
|
324
|
+
Uses distinct rate limiting from job alerts to prevent cross-blocking.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
bot: TelegramBot instance to use for sending
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Function that sends health alerts: fn(chat_id=..., text=...)
|
|
331
|
+
|
|
332
|
+
Usage:
|
|
333
|
+
send_fn = create_send_health(telegram_bot)
|
|
334
|
+
send_fn(chat_id="-100...", text="Health alert message")
|
|
335
|
+
"""
|
|
336
|
+
def send_health(*, chat_id: str, text: str) -> None:
|
|
337
|
+
"""Send a health alert via the standard pipeline.
|
|
338
|
+
|
|
339
|
+
Uses distinct rate_limit_key to prevent job alerts from blocking health alerts.
|
|
340
|
+
"""
|
|
341
|
+
if not bot.configured:
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
if not _should_send_health(chat_id):
|
|
345
|
+
logger.debug(
|
|
346
|
+
"health_alert.rate_limited",
|
|
347
|
+
chat_id=chat_id,
|
|
348
|
+
)
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
bot.send_message(
|
|
353
|
+
text,
|
|
354
|
+
chat_id=chat_id,
|
|
355
|
+
disable_web_page_preview=True,
|
|
356
|
+
)
|
|
357
|
+
except Exception as e:
|
|
358
|
+
logger.warning(
|
|
359
|
+
"health_alert.send_failed",
|
|
360
|
+
chat_id=chat_id,
|
|
361
|
+
error=str(e)[:200],
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
return send_health
|