detectkit 0.12.0__tar.gz → 0.13.1__tar.gz
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.
- {detectkit-0.12.0/detectkit.egg-info → detectkit-0.13.1}/PKG-INFO +1 -1
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/__init__.py +1 -1
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/channels/base.py +143 -61
- detectkit-0.13.1/detectkit/alerting/channels/email.py +464 -0
- detectkit-0.13.1/detectkit/alerting/channels/telegram.py +273 -0
- detectkit-0.13.1/detectkit/alerting/channels/webhook.py +403 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/orchestrator/_base.py +4 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/orchestrator/_decision.py +4 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/orchestrator/_recovery.py +2 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/assets/claude/rules/alerting.md +52 -3
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/assets/claude/rules/detectors.md +5 -5
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/assets/claude/rules/metrics.md +1 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/assets/claude/rules/project.md +1 -1
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +3 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/commands/test_alert.py +2 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/config/metric_config.py +30 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/orchestration/task_manager/_alert_step.py +2 -0
- {detectkit-0.12.0 → detectkit-0.13.1/detectkit.egg-info}/PKG-INFO +1 -1
- detectkit-0.12.0/detectkit/alerting/channels/email.py +0 -214
- detectkit-0.12.0/detectkit/alerting/channels/telegram.py +0 -141
- detectkit-0.12.0/detectkit/alerting/channels/webhook.py +0 -238
- {detectkit-0.12.0 → detectkit-0.13.1}/LICENSE +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/MANIFEST.in +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/README.md +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/_output.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/cli/main.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/config/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/config/profile.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/config/project_config.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/config/validator.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/core/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/core/interval.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/core/models.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/manager.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/database/tables.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/detectors/base.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit/utils/stats.py +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/pyproject.toml +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/requirements.txt +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/setup.cfg +0 -0
- {detectkit-0.12.0 → detectkit-0.13.1}/setup.py +0 -0
|
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
|
|
|
4
4
|
A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.13.1"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -67,6 +67,14 @@ class AlertData:
|
|
|
67
67
|
description: str | None = None
|
|
68
68
|
mentions: list[str] = field(default_factory=list)
|
|
69
69
|
project_name: str | None = None
|
|
70
|
+
# Optional actionable links surfaced in the message. ``dashboard_url`` is the
|
|
71
|
+
# headline link (rendered natively as a clickable title/link on
|
|
72
|
+
# Slack/Mattermost, an ``<a>`` on Telegram, a button in email, and exposed as
|
|
73
|
+
# the ``{dashboard_url}`` template variable). ``links`` adds further
|
|
74
|
+
# ``label -> url`` pairs for advanced use. Both default to empty so existing
|
|
75
|
+
# callers and templates render unchanged.
|
|
76
|
+
dashboard_url: str | None = None
|
|
77
|
+
links: dict[str, str] = field(default_factory=dict)
|
|
70
78
|
# Alert rule (the parameters the alert fired with) — see class docstring.
|
|
71
79
|
min_detectors: int | None = None
|
|
72
80
|
direction_policy: str | None = None
|
|
@@ -171,12 +179,47 @@ class BaseAlertChannel(ABC):
|
|
|
171
179
|
else:
|
|
172
180
|
template = self.get_default_template()
|
|
173
181
|
|
|
174
|
-
|
|
182
|
+
ctx = self.build_context(alert_data)
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
message = template.format(**ctx)
|
|
186
|
+
except (KeyError, ValueError, TypeError):
|
|
187
|
+
# Template has an unknown variable or a format spec that doesn't fit
|
|
188
|
+
# the actual value (e.g. ``{value:.2f}`` in a no-data template where
|
|
189
|
+
# value is a string). Fall back to the kind-appropriate default.
|
|
190
|
+
if alert_data.is_error:
|
|
191
|
+
fallback = self.get_default_error_template()
|
|
192
|
+
elif alert_data.is_no_data:
|
|
193
|
+
fallback = self.get_default_no_data_template()
|
|
194
|
+
elif alert_data.is_recovery:
|
|
195
|
+
fallback = self.get_default_recovery_template()
|
|
196
|
+
else:
|
|
197
|
+
fallback = self.get_default_template()
|
|
198
|
+
if template == fallback:
|
|
199
|
+
# Already on the default — re-raise instead of recursing.
|
|
200
|
+
raise
|
|
201
|
+
message = self.format_message(alert_data, fallback)
|
|
202
|
+
|
|
203
|
+
return message
|
|
204
|
+
|
|
205
|
+
def build_context(self, alert_data: AlertData) -> dict[str, Any]:
|
|
206
|
+
"""Compute the display-ready variables for *alert_data*.
|
|
207
|
+
|
|
208
|
+
This is the **single source** of the values injected into message
|
|
209
|
+
templates *and* consumed by channels that render natively (the webhook
|
|
210
|
+
attachment fields, the Telegram HTML message, the email HTML card), so
|
|
211
|
+
every surface stays consistent. It does no escaping — each channel
|
|
212
|
+
applies its own (HTML for Telegram/email, markdown for webhook).
|
|
213
|
+
|
|
214
|
+
Returns a dict whose keys are exactly the ``{placeholders}`` the default
|
|
215
|
+
templates use, plus a few extras (``dashboard_url``, ``dashboard_line``).
|
|
216
|
+
"""
|
|
175
217
|
import math
|
|
176
218
|
from datetime import datetime
|
|
177
219
|
|
|
178
220
|
import numpy as np
|
|
179
221
|
|
|
222
|
+
# Format timestamp to string
|
|
180
223
|
ts = alert_data.timestamp
|
|
181
224
|
if isinstance(ts, np.datetime64):
|
|
182
225
|
ts = ts.astype(datetime)
|
|
@@ -248,13 +291,18 @@ class BaseAlertChannel(ABC):
|
|
|
248
291
|
mentions_str = self.format_mentions(alert_data.mentions)
|
|
249
292
|
mentions_line = f"\n{mentions_str}" if mentions_str else ""
|
|
250
293
|
|
|
294
|
+
# Optional dashboard link surfaced both as a raw placeholder and a ready
|
|
295
|
+
# "Dashboard: <url>" line (empty when unset so templates stay clean).
|
|
296
|
+
dashboard_url = alert_data.dashboard_url or ""
|
|
297
|
+
dashboard_line = f"Dashboard: {dashboard_url}\n" if dashboard_url else ""
|
|
298
|
+
|
|
251
299
|
# Project name + synth prefix for templates. Prefix is empty when
|
|
252
300
|
# project_name is None so default templates render cleanly for
|
|
253
301
|
# callers that don't set it.
|
|
254
302
|
project_name = alert_data.project_name or ""
|
|
255
303
|
project_name_prefix = f"[{alert_data.project_name}] " if alert_data.project_name else ""
|
|
256
304
|
|
|
257
|
-
#
|
|
305
|
+
# Status keyword
|
|
258
306
|
if alert_data.is_error:
|
|
259
307
|
status = "ERROR"
|
|
260
308
|
elif alert_data.is_no_data:
|
|
@@ -264,54 +312,84 @@ class BaseAlertChannel(ABC):
|
|
|
264
312
|
else:
|
|
265
313
|
status = "ANOMALY"
|
|
266
314
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
315
|
+
return {
|
|
316
|
+
"metric_name": alert_data.metric_name,
|
|
317
|
+
"project_name": project_name,
|
|
318
|
+
"project_name_prefix": project_name_prefix,
|
|
319
|
+
"timestamp": ts_str,
|
|
320
|
+
"timezone": alert_data.timezone,
|
|
321
|
+
"value": value_for_template,
|
|
322
|
+
"value_display": value_display,
|
|
323
|
+
"confidence_lower": alert_data.confidence_lower,
|
|
324
|
+
"confidence_upper": alert_data.confidence_upper,
|
|
325
|
+
"confidence_interval": confidence_str,
|
|
326
|
+
"expected_range": expected_range,
|
|
327
|
+
"detector_name": alert_data.detector_name,
|
|
328
|
+
"detector_count": detector_count,
|
|
329
|
+
"detector_params": alert_data.detector_params,
|
|
330
|
+
"direction": alert_data.direction,
|
|
331
|
+
"direction_policy": direction_policy,
|
|
332
|
+
"min_detectors": min_detectors,
|
|
333
|
+
"severity": alert_data.severity,
|
|
334
|
+
"consecutive_count": alert_data.consecutive_count,
|
|
335
|
+
"consecutive_required": consecutive_required,
|
|
336
|
+
"status": status,
|
|
337
|
+
"error_type": alert_data.error_type or "",
|
|
338
|
+
"error_message": alert_data.error_message or "",
|
|
339
|
+
"description": alert_data.description or "",
|
|
340
|
+
"description_line": description_line,
|
|
341
|
+
"dashboard_url": dashboard_url,
|
|
342
|
+
"dashboard_line": dashboard_line,
|
|
343
|
+
"mentions": mentions_str,
|
|
344
|
+
"mentions_line": mentions_line,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# ---- Status presentation (shared accents across all channels) ----
|
|
348
|
+
# Kept in sync with the brand status tokens (.claude/rules/design.md) and the
|
|
349
|
+
# website status colors so chat, email and dashboards read the same way.
|
|
350
|
+
_STATUS_COLORS = {
|
|
351
|
+
"anomaly": "#D63232",
|
|
352
|
+
"recovery": "#36A64F",
|
|
353
|
+
"no_data": "#F0AD4E",
|
|
354
|
+
"error": "#5A7A8C",
|
|
355
|
+
}
|
|
356
|
+
_STATUS_WORDS = {
|
|
357
|
+
"anomaly": "Anomaly",
|
|
358
|
+
"recovery": "Recovered",
|
|
359
|
+
"no_data": "No data",
|
|
360
|
+
"error": "Pipeline error",
|
|
361
|
+
}
|
|
362
|
+
# Colored status dots — the at-a-glance status cue that leads every alert
|
|
363
|
+
# title/headline (and the only color cue on Telegram, which has no bar).
|
|
364
|
+
_STATUS_EMOJI = {
|
|
365
|
+
"anomaly": "\U0001f534", # red circle
|
|
366
|
+
"recovery": "\U0001f7e2", # green circle
|
|
367
|
+
"no_data": "\U0001f7e1", # yellow circle
|
|
368
|
+
"error": "\U0001f535", # blue circle
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def status_kind(alert_data: AlertData) -> str:
|
|
373
|
+
"""Return the alert kind: ``anomaly`` / ``recovery`` / ``no_data`` / ``error``."""
|
|
374
|
+
if alert_data.is_error:
|
|
375
|
+
return "error"
|
|
376
|
+
if alert_data.is_no_data:
|
|
377
|
+
return "no_data"
|
|
378
|
+
if alert_data.is_recovery:
|
|
379
|
+
return "recovery"
|
|
380
|
+
return "anomaly"
|
|
313
381
|
|
|
314
|
-
|
|
382
|
+
def status_color(self, alert_data: AlertData) -> str:
|
|
383
|
+
"""Accent color for this alert kind (hex)."""
|
|
384
|
+
return self._STATUS_COLORS[self.status_kind(alert_data)]
|
|
385
|
+
|
|
386
|
+
def status_word(self, alert_data: AlertData) -> str:
|
|
387
|
+
"""Human-readable status word for this alert kind."""
|
|
388
|
+
return self._STATUS_WORDS[self.status_kind(alert_data)]
|
|
389
|
+
|
|
390
|
+
def status_emoji(self, alert_data: AlertData) -> str:
|
|
391
|
+
"""Colored status dot for channels without a native color bar."""
|
|
392
|
+
return self._STATUS_EMOJI[self.status_kind(alert_data)]
|
|
315
393
|
|
|
316
394
|
def format_mentions(self, mentions: list[str]) -> str:
|
|
317
395
|
"""
|
|
@@ -372,7 +450,7 @@ class BaseAlertChannel(ABC):
|
|
|
372
450
|
Default template string
|
|
373
451
|
"""
|
|
374
452
|
return (
|
|
375
|
-
"
|
|
453
|
+
"🔴 Alert: {metric_name}\n"
|
|
376
454
|
"{description_line}"
|
|
377
455
|
"Quorum {detector_count}/{min_detectors} · "
|
|
378
456
|
"direction {direction} (policy {direction_policy}) · "
|
|
@@ -385,7 +463,8 @@ class BaseAlertChannel(ABC):
|
|
|
385
463
|
"· Value: {value_display} | Expected: {expected_range}\n"
|
|
386
464
|
"· Severity: {severity:.2f}\n"
|
|
387
465
|
"Detectors: {detector_name}\n"
|
|
388
|
-
"Parameters: {detector_params}"
|
|
466
|
+
"Parameters: {detector_params}\n"
|
|
467
|
+
"{dashboard_line}"
|
|
389
468
|
"{mentions_line}"
|
|
390
469
|
)
|
|
391
470
|
|
|
@@ -397,7 +476,7 @@ class BaseAlertChannel(ABC):
|
|
|
397
476
|
Default recovery template string
|
|
398
477
|
"""
|
|
399
478
|
return (
|
|
400
|
-
"
|
|
479
|
+
"🟢 Alert cleared: {metric_name}\n"
|
|
401
480
|
"{description_line}"
|
|
402
481
|
"The alert condition no longer holds — "
|
|
403
482
|
"the metric is back within expected bounds.\n"
|
|
@@ -407,7 +486,8 @@ class BaseAlertChannel(ABC):
|
|
|
407
486
|
"Latest point:\n"
|
|
408
487
|
"· Time: {timestamp}\n"
|
|
409
488
|
"· Value: {value_display} | Expected: {expected_range}\n"
|
|
410
|
-
"Detectors: {detector_name}"
|
|
489
|
+
"Detectors: {detector_name}\n"
|
|
490
|
+
"{dashboard_line}"
|
|
411
491
|
"{mentions_line}"
|
|
412
492
|
)
|
|
413
493
|
|
|
@@ -420,7 +500,7 @@ class BaseAlertChannel(ABC):
|
|
|
420
500
|
Returns:
|
|
421
501
|
Default title template string
|
|
422
502
|
"""
|
|
423
|
-
return "
|
|
503
|
+
return "🔴 Alert: {metric_name}"
|
|
424
504
|
|
|
425
505
|
def get_default_recovery_title_template(self) -> str:
|
|
426
506
|
"""
|
|
@@ -429,7 +509,7 @@ class BaseAlertChannel(ABC):
|
|
|
429
509
|
Returns:
|
|
430
510
|
Default recovery title template string
|
|
431
511
|
"""
|
|
432
|
-
return "
|
|
512
|
+
return "🟢 Alert cleared: {metric_name}"
|
|
433
513
|
|
|
434
514
|
def get_default_no_data_template(self) -> str:
|
|
435
515
|
"""
|
|
@@ -439,24 +519,26 @@ class BaseAlertChannel(ABC):
|
|
|
439
519
|
has no datapoint (no row OR row with NULL/NaN value).
|
|
440
520
|
"""
|
|
441
521
|
return (
|
|
442
|
-
"No data for metric: {metric_name}\n"
|
|
522
|
+
"🟡 No data for metric: {metric_name}\n"
|
|
443
523
|
"{description_line}"
|
|
444
524
|
"Time: {timestamp}\n"
|
|
445
|
-
"Status: query returned no datapoint for the latest interval"
|
|
525
|
+
"Status: query returned no datapoint for the latest interval\n"
|
|
526
|
+
"{dashboard_line}"
|
|
446
527
|
"{mentions_line}"
|
|
447
528
|
)
|
|
448
529
|
|
|
449
530
|
def get_default_no_data_title_template(self) -> str:
|
|
450
531
|
"""Get default title template for no-data alerts."""
|
|
451
|
-
return "No data: {metric_name}"
|
|
532
|
+
return "🟡 No data: {metric_name}"
|
|
452
533
|
|
|
453
534
|
def get_default_error_template(self) -> str:
|
|
454
535
|
"""Default body template for project-level error alerts."""
|
|
455
536
|
return (
|
|
456
|
-
"Pipeline failed for metric: {metric_name}\n"
|
|
537
|
+
"🔵 Pipeline failed for metric: {metric_name}\n"
|
|
457
538
|
"{description_line}"
|
|
458
539
|
"Time: {timestamp}\n"
|
|
459
|
-
"Error: {error_type}: {error_message}"
|
|
540
|
+
"Error: {error_type}: {error_message}\n"
|
|
541
|
+
"{dashboard_line}"
|
|
460
542
|
"{mentions_line}"
|
|
461
543
|
)
|
|
462
544
|
|
|
@@ -468,7 +550,7 @@ class BaseAlertChannel(ABC):
|
|
|
468
550
|
stay distinguishable. The prefix collapses to an empty string
|
|
469
551
|
when ``AlertData.project_name`` is None.
|
|
470
552
|
"""
|
|
471
|
-
return "{project_name_prefix}Pipeline error: {metric_name}"
|
|
553
|
+
return "🔵 {project_name_prefix}Pipeline error: {metric_name}"
|
|
472
554
|
|
|
473
555
|
def __repr__(self) -> str:
|
|
474
556
|
"""String representation of channel."""
|