detectkit 0.15.0__tar.gz → 0.16.0__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.15.0/detectkit.egg-info → detectkit-0.16.0}/PKG-INFO +1 -1
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/__init__.py +1 -1
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/channels/base.py +20 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/channels/branding.py +10 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/channels/email.py +15 -2
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/channels/telegram.py +5 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/channels/webhook.py +9 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/orchestrator/_base.py +5 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/orchestrator/_decision.py +2 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/orchestrator/_recovery.py +1 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/rules/alerting.md +30 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/rules/project.md +21 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/commands/test_alert.py +13 -1
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/config/project_config.py +58 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/orchestration/error_dispatch.py +6 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/orchestration/task_manager/_alert_step.py +7 -0
- {detectkit-0.15.0 → detectkit-0.16.0/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.15.0 → detectkit-0.16.0}/LICENSE +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/MANIFEST.in +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/README.md +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/core/models.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/pyproject.toml +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/requirements.txt +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/setup.cfg +0 -0
- {detectkit-0.15.0 → detectkit-0.16.0}/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.16.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -9,6 +9,8 @@ from abc import ABC, abstractmethod
|
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
+
from detectkit.alerting.channels.branding import ALERT_GUIDE_LABEL
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
@dataclass
|
|
14
16
|
class AlertData:
|
|
@@ -79,6 +81,12 @@ class AlertData:
|
|
|
79
81
|
# callers and templates render unchanged.
|
|
80
82
|
dashboard_url: str | None = None
|
|
81
83
|
links: dict[str, str] = field(default_factory=dict)
|
|
84
|
+
# "How to read this alert" link surfaced on every default-rendered message so
|
|
85
|
+
# non-operator stakeholders can click through to a plain-language guide. The
|
|
86
|
+
# orchestrator resolves it from ``ProjectConfig.alert_help_url`` (defaulting to
|
|
87
|
+
# the official docs); direct-API callers leave it ``None`` and render unchanged.
|
|
88
|
+
# Exposed to templates as ``{help_url}`` / ``{help_line}``.
|
|
89
|
+
help_url: str | None = None
|
|
82
90
|
# Alert rule (the parameters the alert fired with) — see class docstring.
|
|
83
91
|
min_detectors: int | None = None
|
|
84
92
|
direction_policy: str | None = None
|
|
@@ -300,6 +308,11 @@ class BaseAlertChannel(ABC):
|
|
|
300
308
|
dashboard_url = alert_data.dashboard_url or ""
|
|
301
309
|
dashboard_line = f"Dashboard: {dashboard_url}\n" if dashboard_url else ""
|
|
302
310
|
|
|
311
|
+
# "How to read this alert" link (same shape as dashboard): a raw
|
|
312
|
+
# placeholder plus a ready-to-drop line, both empty when unset.
|
|
313
|
+
help_url = alert_data.help_url or ""
|
|
314
|
+
help_line = f"{ALERT_GUIDE_LABEL}: {help_url}\n" if help_url else ""
|
|
315
|
+
|
|
303
316
|
# Project name + synth prefix for templates. Prefix is empty when
|
|
304
317
|
# project_name is None so default templates render cleanly for
|
|
305
318
|
# callers that don't set it.
|
|
@@ -344,6 +357,9 @@ class BaseAlertChannel(ABC):
|
|
|
344
357
|
"description_line": description_line,
|
|
345
358
|
"dashboard_url": dashboard_url,
|
|
346
359
|
"dashboard_line": dashboard_line,
|
|
360
|
+
"help_url": help_url,
|
|
361
|
+
"help_line": help_line,
|
|
362
|
+
"help_label": ALERT_GUIDE_LABEL,
|
|
347
363
|
"mentions": mentions_str,
|
|
348
364
|
"mentions_line": mentions_line,
|
|
349
365
|
}
|
|
@@ -469,6 +485,7 @@ class BaseAlertChannel(ABC):
|
|
|
469
485
|
"Detectors: {detector_name}\n"
|
|
470
486
|
"Parameters: {detector_params}\n"
|
|
471
487
|
"{dashboard_line}"
|
|
488
|
+
"{help_line}"
|
|
472
489
|
"{mentions_line}"
|
|
473
490
|
)
|
|
474
491
|
|
|
@@ -492,6 +509,7 @@ class BaseAlertChannel(ABC):
|
|
|
492
509
|
"· Value: {value_display} | Expected: {expected_range}\n"
|
|
493
510
|
"Detectors: {detector_name}\n"
|
|
494
511
|
"{dashboard_line}"
|
|
512
|
+
"{help_line}"
|
|
495
513
|
"{mentions_line}"
|
|
496
514
|
)
|
|
497
515
|
|
|
@@ -528,6 +546,7 @@ class BaseAlertChannel(ABC):
|
|
|
528
546
|
"Time: {timestamp}\n"
|
|
529
547
|
"Status: query returned no datapoint for the latest interval\n"
|
|
530
548
|
"{dashboard_line}"
|
|
549
|
+
"{help_line}"
|
|
531
550
|
"{mentions_line}"
|
|
532
551
|
)
|
|
533
552
|
|
|
@@ -543,6 +562,7 @@ class BaseAlertChannel(ABC):
|
|
|
543
562
|
"Time: {timestamp}\n"
|
|
544
563
|
"Error: {error_type}: {error_message}\n"
|
|
545
564
|
"{dashboard_line}"
|
|
565
|
+
"{help_line}"
|
|
546
566
|
"{mentions_line}"
|
|
547
567
|
)
|
|
548
568
|
|
|
@@ -18,3 +18,13 @@ BRAND_USERNAME = "detectkit"
|
|
|
18
18
|
# or opt out of the avatar entirely with ``icon_emoji``. The PNG is generated by
|
|
19
19
|
# ``website/scripts/make-bot-icon.mjs`` and served from ``website/public/``.
|
|
20
20
|
BRAND_ICON_URL = "https://dtk.pipelab.dev/bot-icon.png"
|
|
21
|
+
|
|
22
|
+
# Default "how to read this alert" link surfaced on every default-rendered alert,
|
|
23
|
+
# so non-operator stakeholders (PMs, analysts, on-call) seeing a notification can
|
|
24
|
+
# click through to a plain-language guide explaining what they're looking at. It
|
|
25
|
+
# points at the official docs page by default; a project can redirect it to its
|
|
26
|
+
# own runbook (or hide it) via ``ProjectConfig.alert_help_url`` — the resolved URL
|
|
27
|
+
# is stamped onto ``AlertData.help_url`` by the orchestrator. ``ALERT_GUIDE_LABEL``
|
|
28
|
+
# is the shared link text so every channel reads the same.
|
|
29
|
+
BRAND_ALERT_GUIDE_URL = "https://dtk.pipelab.dev/guides/reading-alerts/"
|
|
30
|
+
ALERT_GUIDE_LABEL = "How to read this alert"
|
|
@@ -12,7 +12,11 @@ from email.utils import formataddr
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
14
|
from detectkit.alerting.channels.base import AlertData, BaseAlertChannel
|
|
15
|
-
from detectkit.alerting.channels.branding import
|
|
15
|
+
from detectkit.alerting.channels.branding import (
|
|
16
|
+
ALERT_GUIDE_LABEL,
|
|
17
|
+
BRAND_ICON_URL,
|
|
18
|
+
BRAND_USERNAME,
|
|
19
|
+
)
|
|
16
20
|
|
|
17
21
|
# Brand palette as hex literals. Email clients (Outlook/Word engine) ignore CSS
|
|
18
22
|
# custom properties, so the values from website/src/styles/brand.css are copied
|
|
@@ -458,11 +462,20 @@ class EmailChannel(BaseAlertChannel):
|
|
|
458
462
|
project_html = (
|
|
459
463
|
f" · {html.escape(alert_data.project_name)}" if alert_data.project_name else ""
|
|
460
464
|
)
|
|
465
|
+
# "How to read this alert" — a clay footer link to the interpretation
|
|
466
|
+
# guide (empty when opted out via alert_help_url: false).
|
|
467
|
+
help_html = ""
|
|
468
|
+
if alert_data.help_url:
|
|
469
|
+
href = html.escape(alert_data.help_url, quote=True)
|
|
470
|
+
help_html = (
|
|
471
|
+
f' · <a href="{href}" style="color:{_CLAY};text-decoration:none;">'
|
|
472
|
+
f"{html.escape(ALERT_GUIDE_LABEL)} →</a>"
|
|
473
|
+
)
|
|
461
474
|
return (
|
|
462
475
|
f'<tr><td bgcolor="{_SURFACE}" style="background-color:{_SURFACE};'
|
|
463
476
|
f"border-top:1px solid {_BORDER};padding:14px 24px;font-family:{_SANS};"
|
|
464
477
|
f'font-size:12px;color:{_FAINT};mso-line-height-rule:exactly;line-height:16px;">'
|
|
465
|
-
f"Sent by detectkit{project_html}{cc_html}</td></tr>"
|
|
478
|
+
f"Sent by detectkit{project_html}{cc_html}{help_html}</td></tr>"
|
|
466
479
|
)
|
|
467
480
|
|
|
468
481
|
def format_mentions(self, mentions: list[str]) -> str:
|
|
@@ -219,6 +219,11 @@ class TelegramChannel(BaseAlertChannel):
|
|
|
219
219
|
for label, url in alert_data.links.items():
|
|
220
220
|
href = html.escape(url, quote=True)
|
|
221
221
|
link_parts.append(f'<a href="{href}">{esc(label)}</a>')
|
|
222
|
+
# "How to read this alert" — always present (unless opted out), so a
|
|
223
|
+
# stakeholder can click through to the interpretation guide.
|
|
224
|
+
if ctx["help_url"]:
|
|
225
|
+
href = html.escape(ctx["help_url"], quote=True)
|
|
226
|
+
link_parts.append(f'<a href="{href}">{esc(ctx["help_label"])}</a>')
|
|
222
227
|
if link_parts:
|
|
223
228
|
lines.append("")
|
|
224
229
|
lines.append(" · ".join(link_parts))
|
|
@@ -303,6 +303,11 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
303
303
|
if link_lines:
|
|
304
304
|
full("Links", "\n".join(link_lines))
|
|
305
305
|
|
|
306
|
+
# "How to read this alert" — the last field, a bare URL (auto-linkified on
|
|
307
|
+
# both Slack and Mattermost) pointing readers at the interpretation guide.
|
|
308
|
+
if ctx["help_url"]:
|
|
309
|
+
full(ctx["help_label"], ctx["help_url"])
|
|
310
|
+
|
|
306
311
|
# A plain-text one-liner for notification previews / unsupported clients.
|
|
307
312
|
if kind == "no_data":
|
|
308
313
|
fallback = f"{title} at {ctx['timestamp']}"
|
|
@@ -357,6 +362,7 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
357
362
|
"Detectors: {detector_name}\n"
|
|
358
363
|
"Parameters: {detector_params}\n"
|
|
359
364
|
"{dashboard_line}"
|
|
365
|
+
"{help_line}"
|
|
360
366
|
"{mentions_line}"
|
|
361
367
|
)
|
|
362
368
|
|
|
@@ -376,6 +382,7 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
376
382
|
"· Value: {value_display} | Expected: {expected_range}\n"
|
|
377
383
|
"Detectors: {detector_name}\n"
|
|
378
384
|
"{dashboard_line}"
|
|
385
|
+
"{help_line}"
|
|
379
386
|
"{mentions_line}"
|
|
380
387
|
)
|
|
381
388
|
|
|
@@ -386,6 +393,7 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
386
393
|
"Time: {timestamp}\n"
|
|
387
394
|
"Status: query returned no datapoint for the latest interval\n"
|
|
388
395
|
"{dashboard_line}"
|
|
396
|
+
"{help_line}"
|
|
389
397
|
"{mentions_line}"
|
|
390
398
|
)
|
|
391
399
|
|
|
@@ -396,6 +404,7 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
396
404
|
"Time: {timestamp}\n"
|
|
397
405
|
"Error: {error_type}: {error_message}\n"
|
|
398
406
|
"{dashboard_line}"
|
|
407
|
+
"{help_line}"
|
|
399
408
|
"{mentions_line}"
|
|
400
409
|
)
|
|
401
410
|
|
|
@@ -26,6 +26,7 @@ class _OrchestratorBase:
|
|
|
26
26
|
dashboard_url: str | None = None,
|
|
27
27
|
links: dict[str, str] | None = None,
|
|
28
28
|
project_name: str | None = None,
|
|
29
|
+
help_url: str | None = None,
|
|
29
30
|
):
|
|
30
31
|
self.metric_name = metric_name
|
|
31
32
|
self.interval = interval
|
|
@@ -43,6 +44,10 @@ class _OrchestratorBase:
|
|
|
43
44
|
# came from — keeps multiple projects sharing one channel distinct
|
|
44
45
|
# while the bot keeps the default brand name + avatar.
|
|
45
46
|
self.project_name = project_name
|
|
47
|
+
# Resolved "how to read this alert" link (from ProjectConfig.alert_help_url,
|
|
48
|
+
# defaulting to the official docs; None when opted out). Stamped onto every
|
|
49
|
+
# AlertData so channels render a guide link for non-operator stakeholders.
|
|
50
|
+
self.help_url = help_url
|
|
46
51
|
|
|
47
52
|
@staticmethod
|
|
48
53
|
def _group_by_timestamp(
|
|
@@ -242,6 +242,7 @@ class _DecisionMixin(_OrchestratorBase):
|
|
|
242
242
|
dashboard_url=self.dashboard_url,
|
|
243
243
|
links=self.links,
|
|
244
244
|
project_name=self.project_name,
|
|
245
|
+
help_url=self.help_url,
|
|
245
246
|
# Alert rule the message foregrounds: configured thresholds plus
|
|
246
247
|
# the observed quorum size that satisfied them.
|
|
247
248
|
min_detectors=self.conditions.min_detectors,
|
|
@@ -302,6 +303,7 @@ class _DecisionMixin(_OrchestratorBase):
|
|
|
302
303
|
dashboard_url=self.dashboard_url,
|
|
303
304
|
links=self.links,
|
|
304
305
|
project_name=self.project_name,
|
|
306
|
+
help_url=self.help_url,
|
|
305
307
|
)
|
|
306
308
|
|
|
307
309
|
def get_last_complete_point(self, now: datetime | None = None) -> datetime:
|
|
@@ -179,6 +179,7 @@ class _RecoveryMixin(_OrchestratorBase):
|
|
|
179
179
|
dashboard_url=self.dashboard_url,
|
|
180
180
|
links=self.links,
|
|
181
181
|
project_name=self.project_name,
|
|
182
|
+
help_url=self.help_url,
|
|
182
183
|
# Echo the rule that had fired so the recovery message names the
|
|
183
184
|
# same alert condition that just cleared.
|
|
184
185
|
min_detectors=self.conditions.min_detectors,
|
|
@@ -151,6 +151,34 @@ gets an inline "Open dashboard" link, and email gets an "Open dashboard" button.
|
|
|
151
151
|
`links` adds extra `label: url` entries alongside it. Both are also exposed to
|
|
152
152
|
custom templates — see `{dashboard_url}` / `{dashboard_line}` below.
|
|
153
153
|
|
|
154
|
+
## "How to read this alert" link
|
|
155
|
+
|
|
156
|
+
Every **default-rendered** alert (anomaly / recovery / no-data / error) on
|
|
157
|
+
**every** channel carries a `How to read this alert` link pointing non-operator
|
|
158
|
+
stakeholders to a plain-language interpretation guide. It defaults to the
|
|
159
|
+
official detectkit guide (`https://dtk.pipelab.dev/guides/reading-alerts/`) — no
|
|
160
|
+
config needed. Control it project-wide with `alert_help_url` in
|
|
161
|
+
`detectkit_project.yml` (tri-state, see `project.md`):
|
|
162
|
+
|
|
163
|
+
- **unset / null** → the official detectkit guide (default URL above)
|
|
164
|
+
- **a URL string** → your own runbook/wiki page instead
|
|
165
|
+
- **`false`** → hide the link entirely
|
|
166
|
+
|
|
167
|
+
Per-channel rendering (defaults only; resolved by
|
|
168
|
+
`ProjectConfig.resolve_alert_help_url`):
|
|
169
|
+
|
|
170
|
+
- **Slack / Mattermost / generic webhook** — a bottom full-width attachment field
|
|
171
|
+
titled `How to read this alert` whose value is the bare URL (auto-linkified on
|
|
172
|
+
both platforms).
|
|
173
|
+
- **Telegram** — appended to the links line (after the optional "Open dashboard"
|
|
174
|
+
link) as an `<a>` link reading `How to read this alert`.
|
|
175
|
+
- **Email** — in the footer, after `Sent by detectkit · <project>` (and any CC),
|
|
176
|
+
a clay-colored `How to read this alert ->` link.
|
|
177
|
+
|
|
178
|
+
Exposed to custom templates as `{help_url}` (raw URL, empty when unset/hidden)
|
|
179
|
+
and `{help_line}` (`How to read this alert: <url>\n`, empty when unset/hidden) —
|
|
180
|
+
mirrors `{dashboard_url}` / `{dashboard_line}`. See the template table below.
|
|
181
|
+
|
|
154
182
|
## How default messages render
|
|
155
183
|
|
|
156
184
|
With no custom `template`, each channel renders a structured, branded message
|
|
@@ -243,6 +271,8 @@ referenced by path). Key variables:
|
|
|
243
271
|
| `{mentions}` / `{mentions_line}` | formatted mentions |
|
|
244
272
|
| `{dashboard_url}` | raw `dashboard_url` (empty string when unset) |
|
|
245
273
|
| `{dashboard_line}` | `Dashboard: <url>\n` when set, else empty (appended to default plain-text templates) |
|
|
274
|
+
| `{help_url}` | raw "How to read this alert" URL (empty when unset/hidden via `alert_help_url`) |
|
|
275
|
+
| `{help_line}` | `How to read this alert: <url>\n` when set, else empty (mirrors `{dashboard_line}`) |
|
|
246
276
|
|
|
247
277
|
> For no-data/error alerts there is no numeric value — avoid `{value:.2f}` in
|
|
248
278
|
> those templates (detectkit falls back to the default template rather than
|
|
@@ -30,10 +30,31 @@ timeouts: # per-step, seconds
|
|
|
30
30
|
detect: 7200 # detect step (default 7200)
|
|
31
31
|
alert: 300 # alert step (default 300)
|
|
32
32
|
|
|
33
|
+
alert_help_url: null # optional, see below — "How to read this alert" link
|
|
34
|
+
|
|
33
35
|
error_alerting: # optional, see below
|
|
34
36
|
enabled: false
|
|
35
37
|
```
|
|
36
38
|
|
|
39
|
+
### `alert_help_url` — "How to read this alert" link
|
|
40
|
+
|
|
41
|
+
Every default-rendered alert on every channel carries a `How to read this alert`
|
|
42
|
+
link for non-operator stakeholders. Tri-state, resolved by
|
|
43
|
+
`ProjectConfig.resolve_alert_help_url`:
|
|
44
|
+
|
|
45
|
+
- **unset / null** (default) → the official detectkit guide
|
|
46
|
+
(`https://dtk.pipelab.dev/guides/reading-alerts/`).
|
|
47
|
+
- **a URL string** → your own runbook/wiki page instead.
|
|
48
|
+
- **`false`** → hide the link entirely.
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
alert_help_url: https://wiki.ops/how-to-read-alerts # custom page
|
|
52
|
+
# alert_help_url: false # hide the link
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Per-channel rendering and the `{help_url}` / `{help_line}` template variables are
|
|
56
|
+
covered in `alerting.md` → "How to read this alert" link.
|
|
57
|
+
|
|
37
58
|
### `error_alerting` — project-scoped failure alerts
|
|
38
59
|
|
|
39
60
|
Catches any exception from a metric's pipeline (DB down, query timeout, lock
|
|
@@ -22,6 +22,7 @@ def create_mock_alert_data(
|
|
|
22
22
|
metric_config: MetricConfig,
|
|
23
23
|
alerting_config,
|
|
24
24
|
timezone_display: str = "UTC",
|
|
25
|
+
help_url: str | None = None,
|
|
25
26
|
) -> AlertData:
|
|
26
27
|
"""
|
|
27
28
|
Create realistic mock AlertData for testing.
|
|
@@ -33,6 +34,8 @@ def create_mock_alert_data(
|
|
|
33
34
|
``metric_config.alerting`` is a list — the test command
|
|
34
35
|
iterates it and passes one entry at a time.
|
|
35
36
|
timezone_display: Timezone for display
|
|
37
|
+
help_url: Resolved "how to read this alert" link to preview (the
|
|
38
|
+
project's ``alert_help_url``); ``None`` renders no help link.
|
|
36
39
|
|
|
37
40
|
Returns:
|
|
38
41
|
AlertData with mock anomaly data
|
|
@@ -89,6 +92,7 @@ def create_mock_alert_data(
|
|
|
89
92
|
mentions=mentions,
|
|
90
93
|
dashboard_url=getattr(alerting_config, "dashboard_url", None),
|
|
91
94
|
links=dict(getattr(alerting_config, "links", {}) or {}),
|
|
95
|
+
help_url=help_url,
|
|
92
96
|
min_detectors=min_detectors,
|
|
93
97
|
direction_policy=direction_policy,
|
|
94
98
|
consecutive_required=consecutive_required,
|
|
@@ -121,6 +125,12 @@ def run_test_alert(metric_name: str, profile: str | None = None):
|
|
|
121
125
|
|
|
122
126
|
metrics_dir_name = project_data.get("metrics_path", "metrics")
|
|
123
127
|
|
|
128
|
+
# Resolve the "how to read this alert" link so the preview matches what real
|
|
129
|
+
# alerts would carry (brand default, a custom URL, or hidden via false).
|
|
130
|
+
from detectkit.config.project_config import resolve_alert_help_url
|
|
131
|
+
|
|
132
|
+
help_url = resolve_alert_help_url(project_data.get("alert_help_url"))
|
|
133
|
+
|
|
124
134
|
# Find metric config
|
|
125
135
|
metrics_dir = project_root / metrics_dir_name
|
|
126
136
|
metric_files = list(metrics_dir.glob("**/*.yml")) + list(metrics_dir.glob("**/*.yaml"))
|
|
@@ -176,7 +186,9 @@ def run_test_alert(metric_name: str, profile: str | None = None):
|
|
|
176
186
|
print(f" Timezone: {timezone_display}")
|
|
177
187
|
print(f" Channels: {', '.join(alerting_config.channels)}\n")
|
|
178
188
|
|
|
179
|
-
alert_data = create_mock_alert_data(
|
|
189
|
+
alert_data = create_mock_alert_data(
|
|
190
|
+
metric_config, alerting_config, timezone_display, help_url=help_url
|
|
191
|
+
)
|
|
180
192
|
|
|
181
193
|
success_count = 0
|
|
182
194
|
for channel_name in alerting_config.channels:
|
|
@@ -9,6 +9,26 @@ from pathlib import Path
|
|
|
9
9
|
from pydantic import BaseModel, Field, field_validator
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def resolve_alert_help_url(value: "str | bool | None") -> "str | None":
|
|
13
|
+
"""Resolve a raw ``alert_help_url`` config value to a concrete URL or None.
|
|
14
|
+
|
|
15
|
+
Shared by :meth:`ProjectConfig.resolve_alert_help_url` and the ``dtk
|
|
16
|
+
test-alert`` preview (which reads the project YAML as a raw dict), so the
|
|
17
|
+
tri-state rule lives in one place:
|
|
18
|
+
|
|
19
|
+
- ``False`` → ``None`` (the link is hidden).
|
|
20
|
+
- a non-empty string → that URL (a custom runbook/wiki page).
|
|
21
|
+
- ``None`` / ``True`` / empty → the official detectkit guide.
|
|
22
|
+
"""
|
|
23
|
+
from detectkit.alerting.channels.branding import BRAND_ALERT_GUIDE_URL
|
|
24
|
+
|
|
25
|
+
if value is False:
|
|
26
|
+
return None
|
|
27
|
+
if isinstance(value, str) and value.strip():
|
|
28
|
+
return value.strip()
|
|
29
|
+
return BRAND_ALERT_GUIDE_URL
|
|
30
|
+
|
|
31
|
+
|
|
12
32
|
class ProjectPathsConfig(BaseModel):
|
|
13
33
|
"""
|
|
14
34
|
Project directory paths configuration.
|
|
@@ -161,6 +181,44 @@ class ProjectConfig(BaseModel):
|
|
|
161
181
|
default=None,
|
|
162
182
|
description="Project-level error alerting (DB outages, query failures, etc.)",
|
|
163
183
|
)
|
|
184
|
+
# "How to read this alert" link surfaced on every default-rendered alert so
|
|
185
|
+
# stakeholders (PMs, analysts, on-call) can click through to a plain-language
|
|
186
|
+
# explanation of what they're seeing. Tri-state:
|
|
187
|
+
# - unset / None → the official detectkit guide (brand default)
|
|
188
|
+
# - a URL string → your own runbook/wiki page instead
|
|
189
|
+
# - false → hide the link entirely
|
|
190
|
+
# Resolved via ``resolve_alert_help_url()`` and stamped onto ``AlertData``.
|
|
191
|
+
alert_help_url: str | bool | None = Field(
|
|
192
|
+
default=None,
|
|
193
|
+
description=(
|
|
194
|
+
"Link to a guide explaining how to read an alert, shown on every "
|
|
195
|
+
"alert. Defaults to the official docs; set a URL for your own page, "
|
|
196
|
+
"or false to hide it."
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@field_validator("alert_help_url")
|
|
201
|
+
@classmethod
|
|
202
|
+
def validate_alert_help_url(cls, v: "str | bool | None") -> "str | bool | None":
|
|
203
|
+
"""A string override must look like an http(s) URL; ``True`` means default."""
|
|
204
|
+
if isinstance(v, str):
|
|
205
|
+
v = v.strip()
|
|
206
|
+
if not v:
|
|
207
|
+
return None # empty string behaves like "use the default"
|
|
208
|
+
if not (v.startswith("http://") or v.startswith("https://")):
|
|
209
|
+
raise ValueError(
|
|
210
|
+
"alert_help_url must be an http(s) URL, false (to hide), "
|
|
211
|
+
"or unset (to use the default)"
|
|
212
|
+
)
|
|
213
|
+
return v
|
|
214
|
+
|
|
215
|
+
def resolve_alert_help_url(self) -> str | None:
|
|
216
|
+
"""Resolve the configured ``alert_help_url`` to a concrete URL or None.
|
|
217
|
+
|
|
218
|
+
Defaults to the official detectkit guide; a string redirects to your own
|
|
219
|
+
page; ``false`` hides the link. See :func:`resolve_alert_help_url`.
|
|
220
|
+
"""
|
|
221
|
+
return resolve_alert_help_url(self.alert_help_url)
|
|
164
222
|
|
|
165
223
|
@field_validator("name")
|
|
166
224
|
@classmethod
|
|
@@ -74,6 +74,11 @@ def dispatch_project_error_alert(
|
|
|
74
74
|
)
|
|
75
75
|
return False
|
|
76
76
|
|
|
77
|
+
# Resolve the project-level "how to read this alert" link (brand default
|
|
78
|
+
# unless overridden / disabled); duck-typed for stub configs in tests.
|
|
79
|
+
help_resolver = getattr(project_config, "resolve_alert_help_url", None)
|
|
80
|
+
help_url = help_resolver() if callable(help_resolver) else None
|
|
81
|
+
|
|
77
82
|
alert_data = AlertData(
|
|
78
83
|
metric_name=metric_name,
|
|
79
84
|
timestamp=np.datetime64(now_utc_naive(), "ms"),
|
|
@@ -93,6 +98,7 @@ def dispatch_project_error_alert(
|
|
|
93
98
|
description=None,
|
|
94
99
|
mentions=cfg.mentions,
|
|
95
100
|
project_name=getattr(project_config, "name", None),
|
|
101
|
+
help_url=help_url,
|
|
96
102
|
)
|
|
97
103
|
|
|
98
104
|
click.echo(
|
|
@@ -48,6 +48,12 @@ class _AlertStepMixin(_TaskManagerBase):
|
|
|
48
48
|
click.echo(" │ Checking alert conditions...")
|
|
49
49
|
alert_config_id = make_alert_config_id(alerting_config)
|
|
50
50
|
|
|
51
|
+
# Resolve the project-level "how to read this alert" link once per
|
|
52
|
+
# alert config (brand default unless overridden / disabled). Duck-typed
|
|
53
|
+
# so a stub project_config in tests without the resolver stays safe.
|
|
54
|
+
help_resolver = getattr(self.project_config, "resolve_alert_help_url", None)
|
|
55
|
+
help_url = help_resolver() if callable(help_resolver) else None
|
|
56
|
+
|
|
51
57
|
orchestrator = AlertOrchestrator(
|
|
52
58
|
metric_name=config.name,
|
|
53
59
|
interval=interval,
|
|
@@ -65,6 +71,7 @@ class _AlertStepMixin(_TaskManagerBase):
|
|
|
65
71
|
dashboard_url=alerting_config.dashboard_url,
|
|
66
72
|
links=alerting_config.links,
|
|
67
73
|
project_name=getattr(self.project_config, "name", None),
|
|
74
|
+
help_url=help_url,
|
|
68
75
|
)
|
|
69
76
|
|
|
70
77
|
last_point = orchestrator.get_last_complete_point()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.15.0 → detectkit-0.16.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|