detectkit 0.40.0__tar.gz → 0.41.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.40.0/detectkit.egg-info → detectkit-0.41.0}/PKG-INFO +1 -1
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/__init__.py +1 -1
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/channels/webhook.py +116 -67
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/rules/alerting.md +19 -9
- {detectkit-0.40.0 → detectkit-0.41.0/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.40.0 → detectkit-0.41.0}/LICENSE +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/MANIFEST.in +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/README.md +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/html_labeler.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/label_server.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/core/models.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/tuning/assets/tune.js +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/tuning/payload.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/pyproject.toml +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/requirements.txt +0 -0
- {detectkit-0.40.0 → detectkit-0.41.0}/setup.cfg +0 -0
- {detectkit-0.40.0 → detectkit-0.41.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.41.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -25,16 +25,22 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
25
25
|
- Slack incoming webhooks
|
|
26
26
|
- Custom webhook endpoints
|
|
27
27
|
|
|
28
|
-
Rendering: the default (no custom ``template``) payload is
|
|
29
|
-
**Slack/Mattermost
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
Rendering: the default (no custom ``template``) anomaly/recovery payload is
|
|
29
|
+
**two stacked Slack/Mattermost attachments** — a colored **base card** that
|
|
30
|
+
is always visible (the title, a short markdown lead with the **Rule** chip,
|
|
31
|
+
the Value / Expected fields and an always-visible compact **Links** field)
|
|
32
|
+
and a neutral **detail card** carrying the verbose tail (Quorum / Severity /
|
|
33
|
+
the anomalous span / Detectors / Parameters) as one markdown ``text`` block.
|
|
34
|
+
Slack and Mattermost natively collapse only an attachment's ``text`` (Slack
|
|
35
|
+
above 700 characters / 5 line breaks, Mattermost above ~200px of rendered
|
|
36
|
+
height) and **never** collapse the ``fields`` grid — so routing the bulk
|
|
37
|
+
into the detail card's ``text`` lets the platform fold it behind a
|
|
38
|
+
"Show more" toggle while the base (value, expected and links) stays in view.
|
|
39
|
+
Short kinds (no-data / error) render as a single base card. A custom
|
|
40
|
+
``template`` degrades to a plain text-only attachment (one opaque string
|
|
41
|
+
that can't be sliced into fields), keeping the color, title and branding.
|
|
42
|
+
Branding (``footer`` + ``footer_icon``) rides on the **last** attachment so
|
|
43
|
+
it sits at the bottom of the whole message, below the folded detail card.
|
|
38
44
|
|
|
39
45
|
Mentions ride in the **top-level** ``text`` (mentions inside attachments do
|
|
40
46
|
not reliably notify on Slack). A ``dashboard_url`` makes the attachment
|
|
@@ -48,7 +54,12 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
48
54
|
"icon_url": "https://.../bot-icon.png", # or "icon_emoji"
|
|
49
55
|
"text": "<!here> ...", # mentions (top level)
|
|
50
56
|
"channel": "#channel", # optional (Slack)
|
|
51
|
-
"attachments": [
|
|
57
|
+
"attachments": [
|
|
58
|
+
# base card — always visible (never folds; fields don't collapse)
|
|
59
|
+
{"color": ..., "title": ..., "fields": [Value, Expected, Links], ...},
|
|
60
|
+
# detail card — neutral, foldable text (anomaly/recovery only)
|
|
61
|
+
{"text": "<verbose tail>", "mrkdwn_in": ["text"]},
|
|
62
|
+
]
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
Branding: the bot defaults to the **detectkit brand avatar** (``icon_url``)
|
|
@@ -177,39 +188,42 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
177
188
|
if template is not None:
|
|
178
189
|
# Custom template → opaque text-only attachment (can't be sliced
|
|
179
190
|
# into fields), but keep the color, title, branding and mrkdwn.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
attachments: list[dict[str, Any]] = [
|
|
192
|
+
{
|
|
193
|
+
"color": color,
|
|
194
|
+
"title": title,
|
|
195
|
+
"text": self.format_message(alert_data, template),
|
|
196
|
+
"mrkdwn_in": ["text"],
|
|
197
|
+
}
|
|
198
|
+
]
|
|
186
199
|
else:
|
|
187
|
-
|
|
200
|
+
attachments = self._build_rich_attachments(alert_data, ctx, color, title)
|
|
188
201
|
|
|
189
|
-
# Dashboard link → clickable
|
|
202
|
+
# Dashboard link → clickable title on the base (first) attachment.
|
|
190
203
|
if alert_data.dashboard_url:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
# Brand the attachment footer (reliable on Slack even when
|
|
194
|
-
# username/icon are locked to the app install)
|
|
195
|
-
# the
|
|
196
|
-
#
|
|
204
|
+
attachments[0]["title_link"] = alert_data.dashboard_url
|
|
205
|
+
|
|
206
|
+
# Brand the LAST attachment's footer (reliable on Slack even when
|
|
207
|
+
# top-level username/icon are locked to the app install) so the brand
|
|
208
|
+
# line sits at the bottom of the whole message — below the folded detail
|
|
209
|
+
# card when one is present. Pair the brand name with the project name
|
|
210
|
+
# when set ("detectkit · my_project") so two projects posting to the same
|
|
211
|
+
# channel stay distinguishable even past the title.
|
|
197
212
|
footer = self.username or BRAND_USERNAME
|
|
198
213
|
if alert_data.project_name:
|
|
199
214
|
footer = f"{footer} · {alert_data.project_name}"
|
|
200
|
-
|
|
215
|
+
attachments[-1]["footer"] = footer
|
|
201
216
|
if self.icon_url:
|
|
202
|
-
|
|
217
|
+
attachments[-1]["footer_icon"] = self.icon_url
|
|
203
218
|
# Slack-only sugar: a real timestamp under the footer. Mattermost
|
|
204
|
-
# ignores it; the human-readable
|
|
205
|
-
# on both.
|
|
219
|
+
# ignores it; the human-readable timestamp rides in the cards on both.
|
|
206
220
|
ts_unix = self._unix_ts(alert_data.timestamp)
|
|
207
221
|
if ts_unix is not None:
|
|
208
|
-
|
|
222
|
+
attachments[-1]["ts"] = ts_unix
|
|
209
223
|
|
|
210
224
|
payload: dict[str, Any] = {
|
|
211
225
|
"username": self.username,
|
|
212
|
-
"attachments":
|
|
226
|
+
"attachments": attachments,
|
|
213
227
|
}
|
|
214
228
|
|
|
215
229
|
# Mentions ride in TOP-LEVEL text — placed inside an attachment they
|
|
@@ -231,29 +245,43 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
231
245
|
|
|
232
246
|
return payload
|
|
233
247
|
|
|
234
|
-
def
|
|
248
|
+
def _build_rich_attachments(
|
|
235
249
|
self,
|
|
236
250
|
alert_data: AlertData,
|
|
237
251
|
ctx: dict[str, Any],
|
|
238
252
|
color: str,
|
|
239
253
|
title: str,
|
|
240
|
-
) -> dict[str, Any]:
|
|
241
|
-
"""Build the default
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
254
|
+
) -> list[dict[str, Any]]:
|
|
255
|
+
"""Build the default attachment(s) for *alert_data*.
|
|
256
|
+
|
|
257
|
+
Returns a **base card** (always visible) and, for anomaly/recovery, a
|
|
258
|
+
neutral **detail card** whose long ``text`` block the chat client folds
|
|
259
|
+
behind "Show more". Slack and Mattermost collapse only an attachment's
|
|
260
|
+
``text`` (never the ``fields`` grid), so the base keeps the value, the
|
|
261
|
+
expected band and the links permanently in view while the verbose tail
|
|
262
|
+
(quorum, severity, the anomalous span, detectors, parameters) rides in
|
|
263
|
+
the foldable detail card. No-data / error are short, so they render as a
|
|
264
|
+
single base card. Both cards render from one payload on Slack and
|
|
265
|
+
Mattermost.
|
|
246
266
|
"""
|
|
247
267
|
kind = self.status_kind(alert_data)
|
|
248
|
-
|
|
268
|
+
base_fields: list[dict[str, Any]] = []
|
|
269
|
+
detail_lines: list[str] = []
|
|
249
270
|
|
|
250
271
|
def short(name: str, value: str) -> None:
|
|
272
|
+
"""Add a 2-col field to the always-visible base card."""
|
|
251
273
|
if value:
|
|
252
|
-
|
|
274
|
+
base_fields.append({"title": name, "value": value, "short": True})
|
|
253
275
|
|
|
254
276
|
def full(name: str, value: str) -> None:
|
|
277
|
+
"""Add a full-width field to the always-visible base card."""
|
|
255
278
|
if value:
|
|
256
|
-
|
|
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}")
|
|
257
285
|
|
|
258
286
|
def code(s: str) -> str:
|
|
259
287
|
return f"`{s}`" if s else ""
|
|
@@ -261,7 +289,8 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
261
289
|
# The configured firing rule, set apart as a bold "Rule" label + an
|
|
262
290
|
# inline-code chip so it reads as "this is the config that fired" at a
|
|
263
291
|
# glance. Backticks render identically on Slack and Mattermost; the bold
|
|
264
|
-
# label is platform-aware (see ``_bold``).
|
|
292
|
+
# label is platform-aware (see ``_bold``). Stays in the always-visible
|
|
293
|
+
# base lead.
|
|
265
294
|
rule_chip = f"{self._bold('Rule')} " + code(
|
|
266
295
|
f"min_detectors={ctx['min_detectors']} · "
|
|
267
296
|
f"direction={ctx['direction_policy']} · "
|
|
@@ -269,35 +298,42 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
269
298
|
)
|
|
270
299
|
|
|
271
300
|
if kind == "anomaly":
|
|
272
|
-
#
|
|
273
|
-
#
|
|
301
|
+
# Base (always visible): the lead (how long it's been going on), the
|
|
302
|
+
# Rule chip, and the value vs the expected band.
|
|
274
303
|
lead = f"{ctx['anomaly_lead']}\n{rule_chip}"
|
|
275
304
|
short("Value", code(ctx["value_display"]))
|
|
276
305
|
short("Expected", code(ctx["expected_range"]))
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
306
|
+
# Detail (folds): quorum, severity, the span, detectors + params.
|
|
307
|
+
detail(
|
|
308
|
+
"Quorum",
|
|
309
|
+
f"{ctx['detector_count']}/{ctx['min_detectors']} · {ctx['direction']}",
|
|
310
|
+
)
|
|
311
|
+
detail("Severity", f"{alert_data.severity:.2f}")
|
|
280
312
|
if ctx["started_display"]:
|
|
281
|
-
|
|
282
|
-
|
|
313
|
+
detail("Anomaly began", ctx["started_display"])
|
|
314
|
+
detail("Latest reading", ctx["timestamp"])
|
|
283
315
|
else:
|
|
284
|
-
|
|
285
|
-
|
|
316
|
+
detail("Detected at", ctx["timestamp"])
|
|
317
|
+
detail("Detectors", code(ctx["detector_name"]))
|
|
286
318
|
if ctx["detector_params"]:
|
|
287
|
-
|
|
319
|
+
# Fenced block on its own lines so it renders as a code block (and
|
|
320
|
+
# adds line breaks that help trip the platform's fold).
|
|
321
|
+
detail_lines.append(
|
|
322
|
+
f"{self._bold('Parameters')}\n```\n{ctx['detector_params']}\n```"
|
|
323
|
+
)
|
|
288
324
|
elif kind == "recovery":
|
|
289
325
|
lead = f"{ctx['recovery_lead']}\n{rule_chip}"
|
|
290
326
|
short("Value", code(ctx["value_display"]))
|
|
291
327
|
short("Expected", code(ctx["expected_range"]))
|
|
292
|
-
#
|
|
293
|
-
#
|
|
328
|
+
# Detail (folds): the incident timeline (onset → fired → recovered)
|
|
329
|
+
# and the detectors. ``detail`` skips the fired line when unknown.
|
|
294
330
|
if ctx["started_display"]:
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
331
|
+
detail("Anomaly began", ctx["started_display"])
|
|
332
|
+
detail("Alert fired", ctx["fired_display"])
|
|
333
|
+
detail("Recovered", ctx["timestamp"])
|
|
298
334
|
else:
|
|
299
|
-
|
|
300
|
-
|
|
335
|
+
detail("Cleared at", ctx["timestamp"])
|
|
336
|
+
detail("Detectors", code(ctx["detector_name"]))
|
|
301
337
|
elif kind == "no_data":
|
|
302
338
|
lead = "Query returned no datapoint for the latest expected interval."
|
|
303
339
|
full("Expected at", ctx["timestamp"])
|
|
@@ -308,12 +344,13 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
308
344
|
err = f"{ctx['error_type']}: {ctx['error_message']}".strip(": ")
|
|
309
345
|
full("Error", code(err))
|
|
310
346
|
|
|
311
|
-
# Links
|
|
312
|
-
#
|
|
313
|
-
#
|
|
314
|
-
#
|
|
315
|
-
# platform's link syntax
|
|
316
|
-
#
|
|
347
|
+
# Links — always visible in the base card. The dashboard / "how to read
|
|
348
|
+
# this alert" links are actionable, so they never fold. Every entry is a
|
|
349
|
+
# compact clickable label, never a raw URL string (a Grafana URL can be a
|
|
350
|
+
# paragraph long once it carries variables; nobody should read that in an
|
|
351
|
+
# alert), rendered in the platform's link syntax and joined by " · " on a
|
|
352
|
+
# single line to stay compact. (The title is also a clickable dashboard
|
|
353
|
+
# link.)
|
|
317
354
|
link_parts = []
|
|
318
355
|
if alert_data.dashboard_url:
|
|
319
356
|
link_parts.append(self._link_markup(alert_data.dashboard_url, "Dashboard"))
|
|
@@ -335,14 +372,26 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
335
372
|
f"(expected {ctx['expected_range']}) at {ctx['timestamp']}"
|
|
336
373
|
)
|
|
337
374
|
|
|
338
|
-
|
|
375
|
+
base: dict[str, Any] = {
|
|
339
376
|
"fallback": fallback,
|
|
340
377
|
"color": color,
|
|
341
378
|
"title": title,
|
|
342
379
|
"text": lead,
|
|
343
|
-
"fields":
|
|
380
|
+
"fields": base_fields,
|
|
344
381
|
"mrkdwn_in": ["text", "fields"],
|
|
345
382
|
}
|
|
383
|
+
attachments: list[dict[str, Any]] = [base]
|
|
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
|
|
346
395
|
|
|
347
396
|
def _bold(self, text: str) -> str:
|
|
348
397
|
"""Render *text* bold in the target platform's markdown.
|
|
@@ -192,15 +192,25 @@ leads with a colored **status circle** — 🔴 anomaly, 🟢 recovery, 🟡 no-
|
|
|
192
192
|
**project name** as a `[name] ` prefix (from `detectkit_project.yml`) — see
|
|
193
193
|
[Project label](#project-label-multi-project-channels) below.
|
|
194
194
|
|
|
195
|
-
- **Slack / Mattermost / generic webhook** —
|
|
196
|
-
|
|
197
|
-
`
|
|
198
|
-
|
|
199
|
-
fields grid
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
195
|
+
- **Slack / Mattermost / generic webhook** — an anomaly/recovery renders as
|
|
196
|
+
**two stacked attachments** so long alerts fold in the channel. Both platforms
|
|
197
|
+
collapse only an attachment's `text` block behind a "Show more" toggle (Slack
|
|
198
|
+
above 700 chars / 5 line breaks; Mattermost above ~200px) and never collapse
|
|
199
|
+
the `fields` grid, so the message splits into:
|
|
200
|
+
- an always-visible **base card** — status-colored accent bar, a clickable
|
|
201
|
+
title (the metric; links to `dashboard_url` when set), a short markdown lead
|
|
202
|
+
(the duration sentence — see "Incident timing" below) with the **Rule** chip
|
|
203
|
+
beneath it, and a compact fields grid kept to **Value / Expected** plus an
|
|
204
|
+
always-visible compact **Links** field (dashboard + extra links + the "how to
|
|
205
|
+
read this alert" guide as clickable labels);
|
|
206
|
+
- a neutral, foldable **detail card** — the verbose tail as one markdown
|
|
207
|
+
`text` block: Quorum / Severity / the anomalous span (Anomaly began → Latest
|
|
208
|
+
reading; began → fired → recovered on recovery) / Detectors / Parameters.
|
|
209
|
+
|
|
210
|
+
No-data / error stay a single base card. The branded footer + footer icon ride
|
|
211
|
+
on the **last** attachment. @mentions ride in the **top-level** message text so
|
|
212
|
+
they notify. A custom `template` instead renders as a single plain text-only
|
|
213
|
+
attachment (color/title/branding kept, no fields grid, no fold split).
|
|
204
214
|
- **Telegram** — default `parse_mode` is now **HTML**. The default message is
|
|
205
215
|
structured and HTML-escaped: a colored status dot (red anomaly / green
|
|
206
216
|
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
|
|
File without changes
|
{detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.40.0 → detectkit-0.41.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.40.0 → detectkit-0.41.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
|