detectkit 0.40.0__tar.gz → 0.42.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.42.0}/PKG-INFO +1 -1
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/__init__.py +1 -1
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/channels/webhook.py +116 -67
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/__init__.py +0 -2
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/CLAUDE.section.md +6 -4
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/alerting.md +19 -9
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/autotune.md +35 -47
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/cli.md +5 -2
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +32 -33
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +2 -1
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/commands/autotune.py +24 -141
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/commands/init.py +3 -2
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/main.py +6 -28
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/reporting/html_report.py +6 -5
- {detectkit-0.40.0 → detectkit-0.42.0/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit.egg-info/SOURCES.txt +0 -2
- detectkit-0.40.0/detectkit/autotune/html_labeler.py +0 -969
- detectkit-0.40.0/detectkit/autotune/label_server.py +0 -177
- {detectkit-0.40.0 → detectkit-0.42.0}/LICENSE +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/MANIFEST.in +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/README.md +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/core/models.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/tuning/assets/tune.js +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/tuning/payload.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/pyproject.toml +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/requirements.txt +0 -0
- {detectkit-0.40.0 → detectkit-0.42.0}/setup.cfg +0 -0
- {detectkit-0.40.0 → detectkit-0.42.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.42.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.
|
|
@@ -15,7 +15,6 @@ from detectkit.autotune._base import AutoTuneError, _AutoTuneBase
|
|
|
15
15
|
from detectkit.autotune._types import ScoringMetric, TuneMode
|
|
16
16
|
from detectkit.autotune.autotuner import AutoTuner, run_autotune_engine
|
|
17
17
|
from detectkit.autotune.config_emitter import compute_run_id, emit_tuned_config
|
|
18
|
-
from detectkit.autotune.html_labeler import render_labeler_html
|
|
19
18
|
from detectkit.autotune.labels import (
|
|
20
19
|
GroundTruth,
|
|
21
20
|
IncidentLabels,
|
|
@@ -39,6 +38,5 @@ __all__ = [
|
|
|
39
38
|
"emit_tuned_config",
|
|
40
39
|
"parse_incident_labels",
|
|
41
40
|
"parse_labels_file",
|
|
42
|
-
"render_labeler_html",
|
|
43
41
|
"run_autotune_engine",
|
|
44
42
|
]
|
|
@@ -17,10 +17,12 @@ run. But you assist far better with **read access to the same database** (e.g. a
|
|
|
17
17
|
database MCP for the project's ClickHouse / PostgreSQL / MySQL): you can inspect
|
|
18
18
|
a metric's series, find real incidents to label for `dtk autotune`, sanity-check
|
|
19
19
|
a metric query before running it, and confirm detections — instead of asking the
|
|
20
|
-
user to run every query by hand. Without it, fall back to
|
|
21
|
-
`dtk
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
user to run every query by hand. Without it, fall back to marking incidents
|
|
21
|
+
visually in `dtk tune --select <metric>` (**Label** mode to drag spans / Threshold
|
|
22
|
+
capture / Lasso the anomaly cloud, or **Review** mode to confirm fired alerts as
|
|
23
|
+
incidents), then **Save incidents** — which `dtk autotune` auto-discovers in
|
|
24
|
+
`incidents/<metric>/` — and to asking the user. This access is optional and
|
|
25
|
+
separate from the `profiles.yml` connection detectkit uses for the pipeline.
|
|
24
26
|
|
|
25
27
|
### Where to look (read the matching file before answering)
|
|
26
28
|
|
|
@@ -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
|
|
@@ -64,28 +64,24 @@ ratios to choose.
|
|
|
64
64
|
## Command
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
|
-
dtk autotune --select <sel> [--incidents FILE] [--
|
|
67
|
+
dtk autotune --select <sel> [--incidents FILE] [--scoring METRIC] \
|
|
68
68
|
[--from DATE] [--to DATE] [--profile NAME] [--force] [--dry-run] [--report]
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
- `--incidents FILE|DIR` — a labels file (below) → **supervised** tuning. May be a
|
|
72
72
|
**directory** (e.g. `incidents/<name>/`): interactive runs prompt to pick a
|
|
73
|
-
version (default newest), non-interactive use the newest.
|
|
73
|
+
version (default newest), non-interactive use the newest. **Omit it and `dtk
|
|
74
|
+
autotune` auto-discovers the newest labels in `incidents/<metric>/`** (the store
|
|
75
|
+
`dtk tune`'s **Save incidents** writes) — so after labeling in `dtk tune` you run
|
|
76
|
+
`dtk autotune --select <metric>` with no flag. With no labels anywhere, an
|
|
74
77
|
interactive terminal prompts to enter incidents inline; declining (or running
|
|
75
78
|
non-interactively) tunes **unsupervised**.
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
editing in place. Beyond click-drag marking it offers **Threshold capture**
|
|
83
|
-
(grab every span above/below a horizontal line in one gesture), **Lasso capture**
|
|
84
|
-
(loop around a cloud of points; each grid-adjacent run, gaps bridged, becomes one
|
|
85
|
-
incident span), **on-chart delete** (each band's ✕, or select + Delete key), and
|
|
86
|
-
**Import file…** (load a labels file you pick). `--no-serve` writes a static
|
|
87
|
-
`metrics/<name>__labeler.html` (Export downloads the file) and exits; `--no-open`
|
|
88
|
-
prints the URL instead of launching a browser.
|
|
79
|
+
- **To label incidents**, use `dtk tune --select <metric>`: switch to **Label**
|
|
80
|
+
mode (drag spans, **Threshold capture** every span past a horizontal line,
|
|
81
|
+
**Lasso** the anomaly cloud) or **Review** mode (confirm fired alerts as
|
|
82
|
+
incidents), then **Save incidents**. That writes versioned files into
|
|
83
|
+
`incidents/<metric>/` — the **same store this command reads** — so the labels feed
|
|
84
|
+
the next supervised tune with no `--incidents` flag (see `cli.md`).
|
|
89
85
|
- `--scoring` — `mcc` (default), `f1`, `f_beta`, `balanced_accuracy`, `roc_auc`,
|
|
90
86
|
`pr_auc`. MCC uses the whole confusion matrix and suits rare anomalies.
|
|
91
87
|
- `--dry-run` — run the search but persist nothing and write no config.
|
|
@@ -115,45 +111,35 @@ incidents:
|
|
|
115
111
|
- {at: "2026-05-11 09:05:00"} # point
|
|
116
112
|
```
|
|
117
113
|
|
|
118
|
-
### Getting labels —
|
|
114
|
+
### Getting labels — label in `dtk tune` first
|
|
119
115
|
|
|
120
|
-
When labels would help, **offer
|
|
116
|
+
When labels would help, **offer to mark incidents in `dtk tune` before asking the
|
|
121
117
|
user to recall timestamps** — it is the easiest, most reliable path:
|
|
122
118
|
|
|
123
|
-
1. Run `dtk
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
3. That writes `incidents/<metric>/<metric>[-<set>]-<UTC>.yml` automatically
|
|
139
|
-
(named after the metric, optional set name as a suffix; versioned —
|
|
140
|
-
re-labeling never overwrites) and the **same command continues into the tuning
|
|
141
|
-
run** on it. No manual file moving.
|
|
142
|
-
4. **Editing over time:** re-running `--label` seeds the page from the metric's
|
|
143
|
-
newest saved set, so labeling can grow across sessions. To re-tune later on
|
|
144
|
-
saved sets, point `--incidents` at the folder (`incidents/<name>/`) —
|
|
145
|
-
interactive runs let the user pick a version, and `--label --incidents <file>`
|
|
146
|
-
opens that exact set for editing. The static `--no-serve` path still exists
|
|
147
|
-
(Export downloads a file you move into `incidents/<name>/`; its **Import
|
|
148
|
-
file…** button loads one back in).
|
|
119
|
+
1. Run `dtk tune --select <name>`. It opens a localhost browser view of the real
|
|
120
|
+
series (use `--no-open` on a remote box and share the printed 127.0.0.1 URL).
|
|
121
|
+
2. Mark the real incidents one of two ways:
|
|
122
|
+
- **Label** mode — drag a span over each incident, or **Threshold capture**
|
|
123
|
+
every span past a horizontal line in one gesture, or **Lasso anomalies** to
|
|
124
|
+
loop a cloud of anomaly dots into per-streak incident spans.
|
|
125
|
+
- **Review** mode — the fired alerts lead; click each alert marker to confirm it
|
|
126
|
+
**valid** (which marks it as a ground-truth incident) or **false alarm**.
|
|
127
|
+
**Confirm all unreviewed valid** does the lot — a clean metric whose alerts
|
|
128
|
+
are all good is validated in a few clicks without hand-drawing spans.
|
|
129
|
+
3. Click **Save incidents**. It writes a versioned
|
|
130
|
+
`incidents/<metric>/<…>.yml` automatically.
|
|
131
|
+
4. Run `dtk autotune --select <name>` with **no `--incidents` flag** — it
|
|
132
|
+
auto-discovers the newest file in `incidents/<metric>/` and tunes supervised on
|
|
133
|
+
it. (You can still pass `--incidents <file-or-dir>` to pick a specific set.)
|
|
149
134
|
|
|
150
135
|
Prefer this whenever the user can *recognise* incidents on a chart but doesn't
|
|
151
136
|
have exact times. If they already know the times (or you found them via a DB
|
|
152
137
|
MCP), write the labels file / inline `incidents:` directly instead. If there are
|
|
153
138
|
no known incidents, run unsupervised — the baseline below is good on its own.
|
|
154
139
|
|
|
155
|
-
With no labels (no `--incidents`, no config `labels_file`,
|
|
156
|
-
entry), tuning falls back to an
|
|
140
|
+
With no labels (no `--incidents`, no config `labels_file`, none auto-discovered
|
|
141
|
+
in `incidents/<metric>/`, no interactive entry), tuning falls back to an
|
|
142
|
+
**unsupervised** objective that blends three
|
|
157
143
|
band-fit / flag-budget terms — `0.4·budget + 0.3·sharpness + 0.3·separation`: a
|
|
158
144
|
smooth one-sided flag-rate budget (no hard cliff), sharpness (a tight,
|
|
159
145
|
well-calibrated confidence interval), and separation (flagged points sitting
|
|
@@ -192,8 +178,10 @@ differs from `seasonality_candidates`, which only narrows the *set of columns*
|
|
|
192
178
|
the search may consider but still runs the search and may choose none.
|
|
193
179
|
|
|
194
180
|
Label precedence (highest first): `--incidents` flag → `labels_file` → inline
|
|
195
|
-
`incidents` →
|
|
196
|
-
|
|
181
|
+
`incidents` → auto-discovered `incidents/<metric>/` (the newest file, e.g. from
|
|
182
|
+
`dtk tune`'s **Save incidents**) → interactive prompt → none (unsupervised).
|
|
183
|
+
`labels_file` and `incidents` are mutually exclusive. `--scoring` likewise
|
|
184
|
+
overrides `scoring_metric`.
|
|
197
185
|
|
|
198
186
|
## The annotated config
|
|
199
187
|
|
|
@@ -67,8 +67,11 @@ Automatically chooses a metric's seasonality, detector type, hyperparameters and
|
|
|
67
67
|
history window, then writes an annotated `metrics/<name>__tuned_<id>.yml`. Reads
|
|
68
68
|
the metric's loaded datapoints (run `dtk run --steps load` first if empty), never
|
|
69
69
|
edits the original, never alerts. `--incidents FILE` enables supervised tuning
|
|
70
|
-
against labeled incidents;
|
|
71
|
-
|
|
70
|
+
against labeled incidents; omit it and autotune **auto-discovers** the newest
|
|
71
|
+
labels in `incidents/<metric>/` (the store `dtk tune`'s **Save incidents** writes
|
|
72
|
+
— label there in Label/Review mode, then just run `dtk autotune`); with no labels
|
|
73
|
+
anywhere, an unsupervised objective is used. `--dry-run` searches without writing.
|
|
74
|
+
`--report [PATH]` writes the same
|
|
72
75
|
self-contained HTML report as `dtk run` for the tuned winner (default
|
|
73
76
|
`reports/<metric>__tuned_<id>.html`; `<dir>` or a `.html` file also accepted).
|
|
74
77
|
Full reference: `autotune.md`.
|
{detectkit-0.40.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md
RENAMED
|
@@ -79,44 +79,39 @@ generalize is rejected.
|
|
|
79
79
|
Labels turn tuning from a good unsupervised default into a config optimised
|
|
80
80
|
against *your* real incidents (it then optimises MCC and also tunes the alert
|
|
81
81
|
window `consecutive_anomalies`). **The easiest, most reliable way to produce them
|
|
82
|
-
is
|
|
82
|
+
is to mark incidents in `dtk tune` — offer this first**, before asking the user to
|
|
83
83
|
recall timestamps:
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
dtk
|
|
86
|
+
dtk tune --select <name>
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
user through it:
|
|
89
|
+
This opens a localhost browser view of the metric's **real** series; on a remote
|
|
90
|
+
machine add `--no-open` and share the printed URL. Walk the user through it:
|
|
92
91
|
|
|
93
92
|
1. Navigate a long/dense series: **scroll to zoom**, double-click to reset, drag
|
|
94
|
-
the **navigator strip** to move the view
|
|
95
|
-
2.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
Export downloads a versioned file you then move into `incidents/<name>/` and pass
|
|
117
|
-
via `--incidents`; its **Import file…** button loads a saved set back in.) To
|
|
118
|
-
re-tune later on a saved set, point `--incidents` at `incidents/<name>/`
|
|
119
|
-
(interactive runs let the user pick a version).
|
|
93
|
+
the **navigator strip** to move the view.
|
|
94
|
+
2. Mark the real incidents in one of two modes:
|
|
95
|
+
- **Label** mode — **drag a span** over each incident (edges to resize, middle
|
|
96
|
+
to move, ✕/Delete to remove). When many outliers are obvious, **Threshold
|
|
97
|
+
capture** grabs every span past a horizontal line in one gesture (set the
|
|
98
|
+
line, pick the side, optionally bridge gaps); **Lasso anomalies** loops a
|
|
99
|
+
freeform cloud of anomaly dots into one incident span per grid-adjacent run.
|
|
100
|
+
- **Review** mode — the fired alerts lead; **click each alert marker** to cycle
|
|
101
|
+
its verdict un-reviewed → **valid** (green) → **false alarm** (slate).
|
|
102
|
+
**Confirming an alert valid marks it as a ground-truth incident**, and
|
|
103
|
+
**Confirm all unreviewed valid** does the lot — so a clean metric whose alerts
|
|
104
|
+
are all good is validated in a few clicks without hand-drawing spans.
|
|
105
|
+
3. Click **Save incidents**. It writes `incidents/<metric>/<…>.yml` automatically
|
|
106
|
+
(versioned — saving never overwrites). Saving doesn't end the session, so you
|
|
107
|
+
can keep tuning; you can also click **Apply** to write the detector config back.
|
|
108
|
+
|
|
109
|
+
**Then run autotune with no `--incidents` flag** — `dtk autotune --select <name>`
|
|
110
|
+
**auto-discovers the newest file in `incidents/<metric>/`** (the same store
|
|
111
|
+
`dtk tune` saves to) and tunes supervised on it. To re-tune on a specific set,
|
|
112
|
+
pass `--incidents <file-or-dir>` (point it at `incidents/<name>/` and interactive
|
|
113
|
+
runs let the user pick a version). Labeling grows across sessions — reopen
|
|
114
|
+
`dtk tune`, mark a few more, **Save incidents** again, re-run autotune.
|
|
120
115
|
|
|
121
116
|
Prefer this whenever the user can *recognise* incidents on a chart but doesn't
|
|
122
117
|
have exact timestamps — it is far easier than dictating times, and they label
|
|
@@ -158,10 +153,14 @@ is good on its own; it just can't learn *which* spikes you specifically care abo
|
|
|
158
153
|
## Step 3 — Run autotune
|
|
159
154
|
|
|
160
155
|
```bash
|
|
161
|
-
# Supervised
|
|
156
|
+
# Supervised — labeled in `dtk tune`: just run it, autotune auto-discovers
|
|
157
|
+
# the newest file in incidents/<name>/ (no --incidents flag needed):
|
|
158
|
+
dtk autotune --select <name>
|
|
159
|
+
|
|
160
|
+
# Supervised — point at a specific labels file or set explicitly:
|
|
162
161
|
dtk autotune --select <name> --incidents incidents/<name>.yml
|
|
163
162
|
|
|
164
|
-
# Unsupervised (no labels — tunes on data statistics):
|
|
163
|
+
# Unsupervised (no labels anywhere — tunes on data statistics):
|
|
165
164
|
dtk autotune --select <name>
|
|
166
165
|
```
|
|
167
166
|
|