detectkit 0.45.1__tar.gz → 0.46.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.45.1/detectkit.egg-info → detectkit-0.46.0}/PKG-INFO +1 -1
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/__init__.py +1 -1
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/channels/webhook.py +145 -129
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/rules/alerting.md +20 -20
- {detectkit-0.45.1 → detectkit-0.46.0/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.45.1 → detectkit-0.46.0}/LICENSE +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/MANIFEST.in +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/README.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/core/models.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/tuning/assets/tune.js +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/tuning/payload.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/pyproject.toml +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/requirements.txt +0 -0
- {detectkit-0.45.1 → detectkit-0.46.0}/setup.cfg +0 -0
- {detectkit-0.45.1 → detectkit-0.46.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.46.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -25,22 +25,23 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
25
25
|
- Slack incoming webhooks
|
|
26
26
|
- Custom webhook endpoints
|
|
27
27
|
|
|
28
|
-
Rendering: the default (no custom ``template``)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
Rendering: the default (no custom ``template``) payload is a **single
|
|
29
|
+
status-colored Slack/Mattermost attachment** whose whole body rides in one
|
|
30
|
+
markdown ``text`` block, ordered most-important-first — the lead + **Rule**
|
|
31
|
+
chip, then Value / Expected, the action **Links**, and finally the verbose
|
|
32
|
+
evidence tail (Quorum / Severity / the anomalous span / Detectors /
|
|
33
|
+
Parameters). Both platforms natively collapse a long attachment ``text``
|
|
34
|
+
behind a **"Show more"** toggle (Slack above 700 characters / 5 line breaks;
|
|
35
|
+
Mattermost wraps only the ``text`` in its ``maxHeight`` 200px fold — the
|
|
36
|
+
``title``, the color bar and the ``footer`` render *outside* that fold), so a
|
|
37
|
+
long anomaly folds its tail exactly like a reference AlertManager alert while
|
|
38
|
+
the one colored bar, the clickable title and the **branded footer (with the
|
|
39
|
+
logo)** stay in view even when the body is collapsed. No-data / error alerts
|
|
40
|
+
stay short, single un-folded cards; a long anomaly — or a full recovery
|
|
41
|
+
timeline (onset → fired → recovered) — folds its tail. A custom ``template``
|
|
42
|
+
renders the same shape — one colored, branded, text-only attachment.
|
|
43
|
+
Branding (``footer`` + ``footer_icon``, the brand logo) rides on that single
|
|
44
|
+
attachment and, being outside the text fold, is always visible.
|
|
44
45
|
|
|
45
46
|
Mentions ride in the **top-level** ``text`` (mentions inside attachments do
|
|
46
47
|
not reliably notify on Slack). A ``dashboard_url`` makes the attachment
|
|
@@ -55,10 +56,16 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
55
56
|
"text": "<!here> ...", # mentions (top level)
|
|
56
57
|
"channel": "#channel", # optional (Slack)
|
|
57
58
|
"attachments": [
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
# one colored card; the long body folds behind "Show more",
|
|
60
|
+
# the footer (brand + logo) stays visible below the fold.
|
|
61
|
+
{
|
|
62
|
+
"color": ...,
|
|
63
|
+
"title": ...,
|
|
64
|
+
"text": "<lead + Rule + value/expected + links + tail>",
|
|
65
|
+
"footer": "detectkit · <project>",
|
|
66
|
+
"footer_icon": "https://.../bot-icon.png",
|
|
67
|
+
"mrkdwn_in": ["text"],
|
|
68
|
+
},
|
|
62
69
|
]
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -134,8 +141,9 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
134
141
|
Args:
|
|
135
142
|
alert_data: Alert data to send
|
|
136
143
|
template: Optional custom message template. When given, the
|
|
137
|
-
attachment carries the formatted template as
|
|
138
|
-
|
|
144
|
+
attachment carries the formatted template verbatim as the body
|
|
145
|
+
text (in place of the structured lead/Value/tail sections);
|
|
146
|
+
otherwise the rich default is built.
|
|
139
147
|
|
|
140
148
|
Returns:
|
|
141
149
|
True if sent successfully, False otherwise
|
|
@@ -199,16 +207,18 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
199
207
|
else:
|
|
200
208
|
attachments = self._build_rich_attachments(alert_data, ctx, color, title)
|
|
201
209
|
|
|
202
|
-
# Dashboard link → clickable title on the
|
|
210
|
+
# Dashboard link → clickable title on the attachment.
|
|
203
211
|
if alert_data.dashboard_url:
|
|
204
212
|
attachments[0]["title_link"] = alert_data.dashboard_url
|
|
205
213
|
|
|
206
|
-
# Brand the
|
|
207
|
-
# top-level username/icon are locked to the app install)
|
|
208
|
-
#
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
214
|
+
# Brand the (single) attachment's footer (reliable on Slack even when
|
|
215
|
+
# top-level username/icon are locked to the app install). Mattermost and
|
|
216
|
+
# Slack render an attachment's footer *outside* the "Show more" text
|
|
217
|
+
# fold, so the brand line — and its logo (``footer_icon``) — stays
|
|
218
|
+
# visible at the bottom of the message even when the body is collapsed.
|
|
219
|
+
# Pair the brand name with the project name when set
|
|
220
|
+
# ("detectkit · my_project") so two projects posting to the same channel
|
|
221
|
+
# stay distinguishable even past the title.
|
|
212
222
|
footer = self.username or BRAND_USERNAME
|
|
213
223
|
if alert_data.project_name:
|
|
214
224
|
footer = f"{footer} · {alert_data.project_name}"
|
|
@@ -252,114 +262,131 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
252
262
|
color: str,
|
|
253
263
|
title: str,
|
|
254
264
|
) -> list[dict[str, Any]]:
|
|
255
|
-
"""Build the default attachment
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
265
|
+
"""Build the default **single** attachment for *alert_data*.
|
|
266
|
+
|
|
267
|
+
Everything rides in one status-colored attachment whose body is a single
|
|
268
|
+
markdown ``text`` block, ordered most-important-first: the lead + Rule
|
|
269
|
+
chip, then value / expected, the action links, and finally the verbose
|
|
270
|
+
evidence tail (quorum, severity, the anomalous span, detectors,
|
|
271
|
+
parameters). Mattermost wraps only an attachment's ``text`` in its
|
|
272
|
+
"Show more" fold (``maxHeight`` 200px) and renders the ``title``, color
|
|
273
|
+
bar and ``footer`` outside it; Slack folds a long ``text`` too. So a long
|
|
274
|
+
anomaly collapses its tail behind "Show more" — exactly the one-block,
|
|
275
|
+
one-color, foldable layout of a reference AlertManager alert — while the
|
|
276
|
+
colored bar, the clickable title and the branded footer/logo (attached in
|
|
277
|
+
:meth:`build_payload`) stay in view even when collapsed. No-data / error
|
|
278
|
+
stay short, single un-folded cards; a long anomaly (or a full recovery
|
|
279
|
+
timeline) folds its tail.
|
|
266
280
|
"""
|
|
267
281
|
kind = self.status_kind(alert_data)
|
|
268
|
-
base_fields: list[dict[str, Any]] = []
|
|
269
|
-
detail_lines: list[str] = []
|
|
270
|
-
|
|
271
|
-
def short(name: str, value: str) -> None:
|
|
272
|
-
"""Add a 2-col field to the always-visible base card."""
|
|
273
|
-
if value:
|
|
274
|
-
base_fields.append({"title": name, "value": value, "short": True})
|
|
275
|
-
|
|
276
|
-
def full(name: str, value: str) -> None:
|
|
277
|
-
"""Add a full-width field to the always-visible base card."""
|
|
278
|
-
if value:
|
|
279
|
-
base_fields.append({"title": name, "value": value, "short": False})
|
|
280
|
-
|
|
281
|
-
def detail(name: str, value: str) -> None:
|
|
282
|
-
"""Add one line to the foldable detail card: bold label + value."""
|
|
283
|
-
if value:
|
|
284
|
-
detail_lines.append(f"{self._bold(name)} {value}")
|
|
285
282
|
|
|
286
283
|
def code(s: str) -> str:
|
|
287
284
|
return f"`{s}`" if s else ""
|
|
288
285
|
|
|
286
|
+
def line(name: str, value: str) -> str:
|
|
287
|
+
"""One ``bold label + value`` body line (empty value → dropped)."""
|
|
288
|
+
return f"{self._bold(name)} {value}" if value else ""
|
|
289
|
+
|
|
289
290
|
# The configured firing rule, set apart as a bold "Rule" label + an
|
|
290
291
|
# inline-code chip so it reads as "this is the config that fired" at a
|
|
291
292
|
# glance. Backticks render identically on Slack and Mattermost; the bold
|
|
292
|
-
# label is platform-aware (see ``_bold``).
|
|
293
|
-
# base lead.
|
|
293
|
+
# label is platform-aware (see ``_bold``). Leads the body, above the fold.
|
|
294
294
|
rule_chip = f"{self._bold('Rule')} " + code(
|
|
295
295
|
f"min_detectors={ctx['min_detectors']} · "
|
|
296
296
|
f"direction={ctx['direction_policy']} · "
|
|
297
297
|
f"consecutive={ctx['consecutive_required']}"
|
|
298
298
|
)
|
|
299
299
|
|
|
300
|
+
# Links — compact clickable labels (never raw URL strings: a Grafana URL
|
|
301
|
+
# can be a paragraph long once it carries variables), rendered in the
|
|
302
|
+
# platform's link syntax and joined by " · ". Kept high in the body so
|
|
303
|
+
# they stay above the fold and actionable at a glance. (The title is also
|
|
304
|
+
# a clickable dashboard link.)
|
|
305
|
+
link_parts: list[str] = []
|
|
306
|
+
if alert_data.dashboard_url:
|
|
307
|
+
link_parts.append(self._link_markup(alert_data.dashboard_url, "Dashboard"))
|
|
308
|
+
for label, url in alert_data.links.items():
|
|
309
|
+
link_parts.append(self._link_markup(url, label))
|
|
310
|
+
if ctx["help_url"]:
|
|
311
|
+
link_parts.append(self._link_markup(ctx["help_url"], ctx["help_label"]))
|
|
312
|
+
links_line = line("Links", " · ".join(link_parts)) if link_parts else ""
|
|
313
|
+
|
|
314
|
+
# The body is built as sections joined by a blank line; the verbose tail
|
|
315
|
+
# is last so the platform fold hides it first.
|
|
316
|
+
sections: list[list[str]] = []
|
|
300
317
|
if kind == "anomaly":
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
"Quorum",
|
|
309
|
-
f"{ctx['detector_count']}/{ctx['min_detectors']} · {ctx['direction']}",
|
|
318
|
+
sections.append([ctx["anomaly_lead"], rule_chip])
|
|
319
|
+
sections.append(
|
|
320
|
+
[
|
|
321
|
+
line("Value", code(ctx["value_display"])),
|
|
322
|
+
line("Expected", code(ctx["expected_range"])),
|
|
323
|
+
links_line,
|
|
324
|
+
]
|
|
310
325
|
)
|
|
311
|
-
|
|
326
|
+
tail = [
|
|
327
|
+
line(
|
|
328
|
+
"Quorum",
|
|
329
|
+
f"{ctx['detector_count']}/{ctx['min_detectors']} · {ctx['direction']}",
|
|
330
|
+
),
|
|
331
|
+
line("Severity", f"{alert_data.severity:.2f}"),
|
|
332
|
+
]
|
|
312
333
|
if ctx["started_display"]:
|
|
313
|
-
|
|
314
|
-
|
|
334
|
+
tail.append(line("Anomaly began", ctx["started_display"]))
|
|
335
|
+
tail.append(line("Latest reading", ctx["timestamp"]))
|
|
315
336
|
else:
|
|
316
|
-
|
|
317
|
-
|
|
337
|
+
tail.append(line("Detected at", ctx["timestamp"]))
|
|
338
|
+
tail.append(line("Detectors", code(ctx["detector_name"])))
|
|
318
339
|
if ctx["detector_params"]:
|
|
319
340
|
# Fenced block on its own lines so it renders as a code block (and
|
|
320
341
|
# adds line breaks that help trip the platform's fold).
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
)
|
|
342
|
+
tail.append(f"{self._bold('Parameters')}\n```\n{ctx['detector_params']}\n```")
|
|
343
|
+
sections.append(tail)
|
|
324
344
|
elif kind == "recovery":
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
345
|
+
sections.append([ctx["recovery_lead"], rule_chip])
|
|
346
|
+
sections.append(
|
|
347
|
+
[
|
|
348
|
+
line("Value", code(ctx["value_display"])),
|
|
349
|
+
line("Expected", code(ctx["expected_range"])),
|
|
350
|
+
links_line,
|
|
351
|
+
]
|
|
352
|
+
)
|
|
353
|
+
# The incident timeline (onset → fired → recovered) + detectors;
|
|
354
|
+
# ``line`` drops the fired entry when unknown.
|
|
330
355
|
if ctx["started_display"]:
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
356
|
+
tail = [
|
|
357
|
+
line("Anomaly began", ctx["started_display"]),
|
|
358
|
+
line("Alert fired", ctx["fired_display"]),
|
|
359
|
+
line("Recovered", ctx["timestamp"]),
|
|
360
|
+
]
|
|
334
361
|
else:
|
|
335
|
-
|
|
336
|
-
|
|
362
|
+
tail = [line("Cleared at", ctx["timestamp"])]
|
|
363
|
+
tail.append(line("Detectors", code(ctx["detector_name"])))
|
|
364
|
+
sections.append(tail)
|
|
337
365
|
elif kind == "no_data":
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
366
|
+
sections.append(["Query returned no datapoint for the latest expected interval."])
|
|
367
|
+
sections.append(
|
|
368
|
+
[
|
|
369
|
+
line("Expected at", ctx["timestamp"]),
|
|
370
|
+
line("Expected", code(ctx["expected_range"])),
|
|
371
|
+
links_line,
|
|
372
|
+
]
|
|
373
|
+
)
|
|
341
374
|
else: # error
|
|
342
|
-
lead = "The detectkit pipeline failed for this metric."
|
|
343
|
-
full("Detected at", ctx["timestamp"])
|
|
344
375
|
err = f"{ctx['error_type']}: {ctx['error_message']}".strip(": ")
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if ctx["help_url"]:
|
|
360
|
-
link_parts.append(self._link_markup(ctx["help_url"], ctx["help_label"]))
|
|
361
|
-
if link_parts:
|
|
362
|
-
full("Links", " · ".join(link_parts))
|
|
376
|
+
sections.append(["The detectkit pipeline failed for this metric."])
|
|
377
|
+
sections.append(
|
|
378
|
+
[
|
|
379
|
+
line("Detected at", ctx["timestamp"]),
|
|
380
|
+
line("Error", code(err)),
|
|
381
|
+
links_line,
|
|
382
|
+
]
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
text = "\n\n".join(
|
|
386
|
+
"\n".join(entry for entry in section if entry)
|
|
387
|
+
for section in sections
|
|
388
|
+
if any(entry for entry in section)
|
|
389
|
+
)
|
|
363
390
|
|
|
364
391
|
# A plain-text one-liner for notification previews / unsupported clients.
|
|
365
392
|
if kind == "no_data":
|
|
@@ -372,26 +399,15 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
372
399
|
f"(expected {ctx['expected_range']}) at {ctx['timestamp']}"
|
|
373
400
|
)
|
|
374
401
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
# Detail card: neutral (no color bar) so it reads as a continuation of
|
|
385
|
-
# the base rather than a second alert, with the verbose tail in one
|
|
386
|
-
# foldable ``text`` block.
|
|
387
|
-
if detail_lines:
|
|
388
|
-
attachments.append(
|
|
389
|
-
{
|
|
390
|
-
"text": "\n".join(detail_lines),
|
|
391
|
-
"mrkdwn_in": ["text"],
|
|
392
|
-
}
|
|
393
|
-
)
|
|
394
|
-
return attachments
|
|
402
|
+
return [
|
|
403
|
+
{
|
|
404
|
+
"fallback": fallback,
|
|
405
|
+
"color": color,
|
|
406
|
+
"title": title,
|
|
407
|
+
"text": text,
|
|
408
|
+
"mrkdwn_in": ["text"],
|
|
409
|
+
}
|
|
410
|
+
]
|
|
395
411
|
|
|
396
412
|
def _bold(self, text: str) -> str:
|
|
397
413
|
"""Render *text* bold in the target platform's markdown.
|
|
@@ -167,7 +167,7 @@ config needed. Control it project-wide with `alert_help_url` in
|
|
|
167
167
|
Per-channel rendering (defaults only; the resolved help URL is rendered per channel as follows):
|
|
168
168
|
|
|
169
169
|
- **Slack / Mattermost / generic webhook** — a clickable `How to read this alert`
|
|
170
|
-
label in the compact `Links`
|
|
170
|
+
label in the compact `Links` line (alongside `Dashboard` + any extra links),
|
|
171
171
|
never a raw URL. Rendered in the platform's link syntax (Slack `<url|label>`,
|
|
172
172
|
Mattermost/generic markdown links) so a long dashboard URL stays hidden behind
|
|
173
173
|
its label.
|
|
@@ -191,25 +191,25 @@ leads with a colored **status circle** — 🔴 anomaly, 🟢 recovery, 🟡 no-
|
|
|
191
191
|
**project name** as a `[name] ` prefix (from `detectkit_project.yml`) — see
|
|
192
192
|
[Project label](#project-label-multi-project-channels) below.
|
|
193
193
|
|
|
194
|
-
- **Slack / Mattermost / generic webhook** — an
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
194
|
+
- **Slack / Mattermost / generic webhook** — an alert renders as **one
|
|
195
|
+
status-colored attachment** whose whole body is a single markdown `text` block,
|
|
196
|
+
ordered **most-important-first** so a long alert folds its tail behind a
|
|
197
|
+
**"Show more"** toggle — one block, one color, just like a reference
|
|
198
|
+
AlertManager alert. The body order: the clickable title (the metric; links to
|
|
199
|
+
`dashboard_url` when set), then the markdown lead (the duration sentence — see
|
|
200
|
+
"Incident timing" below) with the **Rule** chip beneath it, **Value /
|
|
201
|
+
Expected**, the compact **Links** line (dashboard + extra links + the "how to
|
|
202
|
+
read this alert" guide as clickable labels, never raw URLs), and finally the
|
|
203
|
+
verbose tail (Quorum / Severity / the anomalous span — Anomaly began → Latest
|
|
204
|
+
reading; began → fired → recovered on recovery — / Detectors / Parameters).
|
|
205
|
+
Both clients fold **only** the `text` (Slack above 700 chars / 5 line breaks;
|
|
206
|
+
Mattermost above ~200px) and render the title, the color bar and the **footer**
|
|
207
|
+
*outside* the fold — so the branded **footer + footer icon (the logo)** stays
|
|
208
|
+
visible even when the body is collapsed. No-data / error stay short, single
|
|
209
|
+
un-folded cards; a long anomaly (or a full recovery timeline) folds its tail.
|
|
210
|
+
@mentions ride in the **top-level** message text so they notify. A custom
|
|
211
|
+
`template` renders as a single plain text-only attachment (the raw template
|
|
212
|
+
replaces the structured lead/Value/tail sections; color/title/branding kept).
|
|
213
213
|
- **Telegram** — default `parse_mode` is now **HTML**. The default message is
|
|
214
214
|
structured and HTML-escaped: a colored status dot (red anomaly / green
|
|
215
215
|
recovery / yellow no-data / blue error), a bold headline, the lead + rule, then
|
|
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
|
{detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.45.1 → detectkit-0.46.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.45.1 → detectkit-0.46.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
|
|
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
|