detectkit 0.49.1__tar.gz → 0.50.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.49.1/detectkit.egg-info → detectkit-0.50.0}/PKG-INFO +1 -1
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/__init__.py +1 -1
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/orchestrator/_decision.py +2 -1
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/orchestrator/_replay.py +24 -7
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/CLAUDE.section.md +1 -1
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/rules/cli.md +27 -1
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/rules/overview.md +6 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +8 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/config/metric_config.py +4 -3
- detectkit-0.50.0/detectkit/config/metric_io.py +90 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/tuning/config_writer.py +9 -15
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/ui/__init__.py +6 -2
- detectkit-0.50.0/detectkit/ui/assets/ui.js +323 -0
- detectkit-0.50.0/detectkit/ui/metric_files.py +256 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/ui/server.py +220 -9
- {detectkit-0.49.1 → detectkit-0.50.0/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit.egg-info/SOURCES.txt +2 -0
- detectkit-0.49.1/detectkit/ui/assets/ui.js +0 -249
- {detectkit-0.49.1 → detectkit-0.50.0}/LICENSE +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/MANIFEST.in +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/README.md +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/commands/osi.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/commands/ui.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/core/models.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/semantic/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/semantic/errors.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/semantic/exporter.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/semantic/importer.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/semantic/osi_model.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/semantic/query_gen.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/tuning/assets/tune.js +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/tuning/payload.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/ui/html.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/ui/jobs.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/ui/overview.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/pyproject.toml +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/requirements.txt +0 -0
- {detectkit-0.49.1 → detectkit-0.50.0}/setup.cfg +0 -0
- {detectkit-0.49.1 → detectkit-0.50.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.50.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -29,6 +29,7 @@ The multi-detector alert contract (documented in docs/guides/alerting.md):
|
|
|
29
29
|
from __future__ import annotations
|
|
30
30
|
|
|
31
31
|
import math
|
|
32
|
+
from collections.abc import Sequence
|
|
32
33
|
from datetime import datetime, timezone
|
|
33
34
|
|
|
34
35
|
import numpy as np
|
|
@@ -166,7 +167,7 @@ class _DecisionMixin(_OrchestratorBase):
|
|
|
166
167
|
def _count_consecutive_anomalies(
|
|
167
168
|
self,
|
|
168
169
|
detections_by_time: dict[np.datetime64, list[DetectionRecord]],
|
|
169
|
-
timestamps_sorted:
|
|
170
|
+
timestamps_sorted: Sequence[np.datetime64],
|
|
170
171
|
) -> tuple[int, list[DetectionRecord] | None, str | None]:
|
|
171
172
|
"""Walk timestamps newest→oldest counting quorum-satisfying points.
|
|
172
173
|
|
|
@@ -16,6 +16,8 @@ mutating ``_dtk_alert_states`` would be wrong.
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
from collections import deque
|
|
20
|
+
from collections.abc import Sequence
|
|
19
21
|
from dataclasses import dataclass
|
|
20
22
|
from datetime import datetime, timedelta
|
|
21
23
|
|
|
@@ -76,7 +78,24 @@ class _ReplayMixin(_OrchestratorBase):
|
|
|
76
78
|
sim_last_recovery: np.datetime64 | None = None
|
|
77
79
|
events: list[ReplayedEvent] = []
|
|
78
80
|
|
|
81
|
+
# The causal view grows monotonically as the grid advances, so it is
|
|
82
|
+
# maintained incrementally: one sorted pass up front, then each grid
|
|
83
|
+
# step admits the newly-covered timestamps (appendleft keeps ts_desc
|
|
84
|
+
# newest-first at O(1)). Rebuilding the dict + re-sorting per grid
|
|
85
|
+
# point — the previous shape — is O(n^2 log n) and turns a month of a
|
|
86
|
+
# 1-minute metric into a multi-minute replay.
|
|
87
|
+
all_ts_asc = sorted(by_time)
|
|
88
|
+
causal: dict[np.datetime64, list[DetectionRecord]] = {}
|
|
89
|
+
ts_desc: deque[np.datetime64] = deque()
|
|
90
|
+
next_ts = 0
|
|
91
|
+
|
|
79
92
|
for t in self._replay_grid(start, end):
|
|
93
|
+
while next_ts < len(all_ts_asc) and all_ts_asc[next_ts] <= t:
|
|
94
|
+
ts = all_ts_asc[next_ts]
|
|
95
|
+
causal[ts] = by_time[ts]
|
|
96
|
+
ts_desc.appendleft(ts)
|
|
97
|
+
next_ts += 1
|
|
98
|
+
|
|
80
99
|
# No-data fires independently of the quorum (a single binary
|
|
81
100
|
# metric-level signal), only when configured and not in cooldown.
|
|
82
101
|
if (
|
|
@@ -92,9 +111,6 @@ class _ReplayMixin(_OrchestratorBase):
|
|
|
92
111
|
sim_last_alert = t
|
|
93
112
|
continue
|
|
94
113
|
|
|
95
|
-
causal = {ts: recs for ts, recs in by_time.items() if ts <= t}
|
|
96
|
-
ts_desc = sorted(causal, reverse=True)
|
|
97
|
-
|
|
98
114
|
consecutive, latest_quorum, direction = self._count_consecutive_anomalies(
|
|
99
115
|
causal, ts_desc
|
|
100
116
|
)
|
|
@@ -170,7 +186,7 @@ class _ReplayMixin(_OrchestratorBase):
|
|
|
170
186
|
def _replay_recovered(
|
|
171
187
|
self,
|
|
172
188
|
causal: dict[np.datetime64, list[DetectionRecord]],
|
|
173
|
-
ts_desc:
|
|
189
|
+
ts_desc: Sequence[np.datetime64],
|
|
174
190
|
sim_last_alert: np.datetime64,
|
|
175
191
|
) -> bool:
|
|
176
192
|
"""Pure half of :meth:`_RecoveryMixin._check_recovery_since_last_alert`.
|
|
@@ -184,8 +200,9 @@ class _ReplayMixin(_OrchestratorBase):
|
|
|
184
200
|
return True
|
|
185
201
|
|
|
186
202
|
# No fresh detections after the alert → assume recovery (mirrors the
|
|
187
|
-
# live "no fresh detections" branch).
|
|
188
|
-
|
|
203
|
+
# live "no fresh detections" branch). ts_desc is newest-first, so the
|
|
204
|
+
# head carries the maximum — no full scan needed.
|
|
205
|
+
if not ts_desc[0] > sim_last_alert:
|
|
189
206
|
return True
|
|
190
207
|
|
|
191
208
|
latest_ts = ts_desc[0]
|
|
@@ -242,7 +259,7 @@ class _ReplayMixin(_OrchestratorBase):
|
|
|
242
259
|
def _replay_streak(
|
|
243
260
|
self,
|
|
244
261
|
causal: dict[np.datetime64, list[DetectionRecord]],
|
|
245
|
-
ts_desc:
|
|
262
|
+
ts_desc: Sequence[np.datetime64],
|
|
246
263
|
) -> tuple[int, np.datetime64, bool]:
|
|
247
264
|
"""In-memory analog of :meth:`_DecisionMixin._resolve_streak`.
|
|
248
265
|
|
|
@@ -40,7 +40,7 @@ version — **read the relevant one on demand** instead of guessing:
|
|
|
40
40
|
| Alert rules (quorum/direction/consecutive), cooldown, recovery, templates | `.claude/rules/detectkit/alerting.md` |
|
|
41
41
|
| Auto-tuning a detector (`dtk autotune`), labels file, the `autotune:` block, `_dtk_autotune_runs` | `.claude/rules/detectkit/autotune.md` |
|
|
42
42
|
| Manually/interactively tuning a detector in the browser and writing it back in place (`dtk tune`) | `.claude/rules/detectkit/cli.md` |
|
|
43
|
-
| A project-wide live overview
|
|
43
|
+
| A project-wide live overview, driving the pipeline, and creating/editing/deleting metric YAMLs from the browser (`dtk ui`) | `.claude/rules/detectkit/cli.md` |
|
|
44
44
|
| OSI (Open Semantic Interchange) interop: import an OSI metric into a native metric / export back (`dtk osi`) | `.claude/rules/detectkit/cli.md` |
|
|
45
45
|
|
|
46
46
|
### Skills
|
|
@@ -12,7 +12,7 @@ Run all commands from a project directory (the one containing
|
|
|
12
12
|
| `dtk run --select <sel>` | Run the load → detect → alert pipeline |
|
|
13
13
|
| `dtk autotune --select <sel>` | Auto-configure a metric's detector (see `autotune.md`) |
|
|
14
14
|
| `dtk tune --select <sel>` | Interactively tune a detector on real data, write it back in place |
|
|
15
|
-
| `dtk ui` | Project-wide live overview + browser pipeline control panel |
|
|
15
|
+
| `dtk ui` | Project-wide live overview + browser pipeline control panel + create/edit/delete metric YAMLs |
|
|
16
16
|
| `dtk test-alert <metric>` | Send a mock alert to the metric's channels |
|
|
17
17
|
| `dtk unlock --select <sel>` | Clear a stuck pipeline lock |
|
|
18
18
|
| `dtk clean --select <sel>` | Prune internal data that no longer matches the config |
|
|
@@ -210,6 +210,32 @@ its own isolated, lock-free session. The `dtk ui` server itself takes **no
|
|
|
210
210
|
pipeline lock** and never mutates anything — every spawned command behaves
|
|
211
211
|
exactly as if typed into a terminal.
|
|
212
212
|
|
|
213
|
+
### Managing metrics from the UI
|
|
214
|
+
|
|
215
|
+
The cockpit header has a **New metric** button — an editor overlay seeded
|
|
216
|
+
with a starter YAML template, with an optional subfolder under `metrics/`;
|
|
217
|
+
saving writes `metrics/[<folder>/]<name>.yml`. Every metric row also has an
|
|
218
|
+
**Edit** action that opens its raw YAML in the same editor. **Save** validates
|
|
219
|
+
server-side **before any write**: YAML syntax → full `MetricConfig` → a deep
|
|
220
|
+
detector-params check (each factory-known detector is actually constructed) —
|
|
221
|
+
an invalid config lands in the editor's error pane with nothing written. A
|
|
222
|
+
successful save **archives the previous file verbatim** to
|
|
223
|
+
`metrics/.history/<metric>/` (the same archive `dtk tune`'s Apply writes,
|
|
224
|
+
excluded from metric discovery) and only then overwrites in place, so the
|
|
225
|
+
text typed is what lands on disk, comments intact (normalized only to end
|
|
226
|
+
with a newline). A save is refused when the file changed on disk after the
|
|
227
|
+
editor was opened (a `dtk tune` Apply or another session saved first) —
|
|
228
|
+
reopen the metric rather than overwrite it. Renaming via `name:`
|
|
229
|
+
is allowed — uniqueness is enforced project-wide — and rows under the old name
|
|
230
|
+
stay in the `_dtk_*` tables until `dtk clean`. **Delete** lives in the edit
|
|
231
|
+
overlay behind an explicit confirmation step, and the server additionally
|
|
232
|
+
requires the request to echo the metric name back; it archives the file
|
|
233
|
+
(`…-deleted.yml`) before removing it, so a delete is reversible by hand, and
|
|
234
|
+
its `_dtk_*` rows likewise remain until `dtk clean`. While a `dtk tune` session
|
|
235
|
+
for that metric is running, Save and Delete are refused (its Apply would race
|
|
236
|
+
the edit). Like the rest of `dtk ui`, metric management takes no pipeline lock
|
|
237
|
+
and never touches the database — it only manages metric YAML files.
|
|
238
|
+
|
|
213
239
|
## `dtk test-alert <metric>`
|
|
214
240
|
|
|
215
241
|
Sends a mock alert (fake value/CI/severity) through the metric's configured
|
|
@@ -97,6 +97,12 @@ series, watch the band recompute live, then write the config back into the metri
|
|
|
97
97
|
— use `dtk tune --select <m>`, the interactive sibling of `dtk autotune`
|
|
98
98
|
(`cli.md`).
|
|
99
99
|
|
|
100
|
+
For a **live**, project-wide view instead of a one-off file, run `dtk ui`: the
|
|
101
|
+
same reports in a browser overlay plus an overview table (alert frequency,
|
|
102
|
+
freshness, quality when incidents are labeled), a panel that drives
|
|
103
|
+
`dtk run` / `dtk autotune` / `dtk unlock`, and an in-browser editor to create,
|
|
104
|
+
edit and delete metric YAML files. See `cli.md`.
|
|
105
|
+
|
|
100
106
|
## Glossary
|
|
101
107
|
|
|
102
108
|
- **metric** — a named time series (SQL + interval) you monitor.
|
{detectkit-0.49.1 → detectkit-0.50.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md
RENAMED
|
@@ -15,6 +15,14 @@ need detail on any field, read the matching file under
|
|
|
15
15
|
`.claude/rules/detectkit/` (`metrics.md`, `detectors.md`, `alerting.md`,
|
|
16
16
|
`project.md`); this skill is the procedure, those are the reference.
|
|
17
17
|
|
|
18
|
+
`dtk ui` also has a browser editor for metric YAMLs (a **New metric** button
|
|
19
|
+
and, per row, an **Edit** action), validated the same way before it writes.
|
|
20
|
+
Prefer this skill when scaffolding from scratch — it gathers the query,
|
|
21
|
+
detector and alerting choices and explains each one in context; prefer the UI
|
|
22
|
+
for a quick manual edit while you're already watching the cockpit (nudging a
|
|
23
|
+
threshold, adding a channel) — it's a raw-YAML editor with no procedure behind
|
|
24
|
+
it.
|
|
25
|
+
|
|
18
26
|
## Step 0 — Confirm you're in a detectkit project
|
|
19
27
|
|
|
20
28
|
A project root contains `detectkit_project.yml`. Verify it exists in the target
|
|
@@ -890,8 +890,9 @@ class MetricConfig(BaseModel):
|
|
|
890
890
|
if not data:
|
|
891
891
|
raise ValueError(f"Empty metric config file: {path}")
|
|
892
892
|
|
|
893
|
-
# Support nested structure: metric: { ... }
|
|
894
|
-
|
|
895
|
-
|
|
893
|
+
# Support nested structure: metric: { ... } (shared unwrap seam)
|
|
894
|
+
from detectkit.config.metric_io import unwrap_metric_mapping
|
|
895
|
+
|
|
896
|
+
data = unwrap_metric_mapping(data)
|
|
896
897
|
|
|
897
898
|
return cls.model_validate(data)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Shared low-level seams for metric YAML files.
|
|
2
|
+
|
|
3
|
+
Three tiny helpers that were previously re-implemented per caller and had
|
|
4
|
+
already started to drift (``tuning/config_writer.py``'s archive write had no
|
|
5
|
+
same-second collision handling; the nested-form unwrap lived in four copies):
|
|
6
|
+
|
|
7
|
+
- :func:`unwrap_metric_mapping` — the nested ``metric: {...}`` form accepted
|
|
8
|
+
everywhere a metric YAML is read (``MetricConfig.from_yaml_file``, the tune
|
|
9
|
+
write-back, the ``dtk ui`` editor).
|
|
10
|
+
- :func:`safe_metric_stem` — a metric name reduced to one safe path component
|
|
11
|
+
(filenames, ``.history`` archive keys).
|
|
12
|
+
- :func:`archive_metric_text` — the ``metrics/.history/<metric>/`` verbatim
|
|
13
|
+
archive convention shared by ``dtk tune``'s Apply and ``dtk ui``'s
|
|
14
|
+
update/delete, collision-safe within one UTC second.
|
|
15
|
+
|
|
16
|
+
No imports from the rest of detectkit — safe to use from any layer.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
_SAFE_CHAR_RE = re.compile(r"[A-Za-z0-9_.\-]")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def unwrap_metric_mapping(data: dict[str, Any]) -> dict[str, Any]:
|
|
30
|
+
"""The metric body of a parsed YAML mapping — unwraps the nested ``metric: {...}`` form.
|
|
31
|
+
|
|
32
|
+
Returns the **same object reference** for the flat form and the nested
|
|
33
|
+
body for the wrapped form, so in-place mutations (the tune write-back
|
|
34
|
+
edits the body inside the original document) behave identically.
|
|
35
|
+
"""
|
|
36
|
+
nested = data.get("metric")
|
|
37
|
+
return nested if isinstance(nested, dict) else data
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def safe_metric_stem(name: str) -> str:
|
|
41
|
+
"""A single safe path component for a metric name — sanitized, never refused.
|
|
42
|
+
|
|
43
|
+
``MetricConfig`` accepts names (unicode letters, a leading ``-``) that make
|
|
44
|
+
unsafe or awkward filenames; rather than rejecting a valid metric, every
|
|
45
|
+
character outside the safe set is replaced with ``_`` and leading dots or
|
|
46
|
+
dashes are stripped. Path separators can never survive, so the result is
|
|
47
|
+
always one component; a same-stem collision surfaces as "file already
|
|
48
|
+
exists". Used for created files' stems and ``.history`` archive keys.
|
|
49
|
+
"""
|
|
50
|
+
stem = "".join(c if _SAFE_CHAR_RE.fullmatch(c) else "_" for c in name)
|
|
51
|
+
stem = stem.lstrip(".-")
|
|
52
|
+
return stem or "metric"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def metric_stamp(now: datetime | None = None) -> str:
|
|
56
|
+
"""UTC filesystem-safe timestamp (``20260709T101530Z``) used in archive filenames."""
|
|
57
|
+
now = now or datetime.now(timezone.utc)
|
|
58
|
+
return now.strftime("%Y%m%dT%H%M%SZ")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def archive_metric_text(
|
|
62
|
+
project_root: Path,
|
|
63
|
+
metric_name: str,
|
|
64
|
+
text: str,
|
|
65
|
+
*,
|
|
66
|
+
suffix: str = "",
|
|
67
|
+
stamp: str | None = None,
|
|
68
|
+
now: datetime | None = None,
|
|
69
|
+
) -> Path:
|
|
70
|
+
"""Write *text* verbatim into ``metrics/.history/<key>/`` and return the path.
|
|
71
|
+
|
|
72
|
+
The directory key is the **sanitized** metric name (:func:`safe_metric_stem`)
|
|
73
|
+
— at archive time the name can be attacker/editor-influenced free text
|
|
74
|
+
(an on-disk ``name:`` changed after boot), and joining it raw into a path
|
|
75
|
+
would let ``../`` or an absolute component escape ``metrics/.history/``.
|
|
76
|
+
Two archives within the same UTC second get ``-1``, ``-2``, … suffixes so
|
|
77
|
+
a tune Apply and a UI save landing together never overwrite each other's
|
|
78
|
+
snapshot.
|
|
79
|
+
"""
|
|
80
|
+
key = safe_metric_stem(metric_name)
|
|
81
|
+
archive_dir = project_root / "metrics" / ".history" / key
|
|
82
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
base = f"{key}-{stamp or metric_stamp(now)}{suffix}"
|
|
84
|
+
archive_path = archive_dir / f"{base}.yml"
|
|
85
|
+
counter = 1
|
|
86
|
+
while archive_path.exists():
|
|
87
|
+
archive_path = archive_dir / f"{base}-{counter}.yml"
|
|
88
|
+
counter += 1
|
|
89
|
+
archive_path.write_text(text, encoding="utf-8")
|
|
90
|
+
return archive_path
|
|
@@ -18,13 +18,14 @@ others and, when an alert used ``min_detectors >= 2``, permanently killing it.
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
from dataclasses import dataclass
|
|
21
|
-
from datetime import datetime
|
|
21
|
+
from datetime import datetime
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
25
|
import yaml
|
|
26
26
|
|
|
27
27
|
from detectkit.config.metric_config import MetricConfig
|
|
28
|
+
from detectkit.config.metric_io import archive_metric_text, metric_stamp, unwrap_metric_mapping
|
|
28
29
|
from detectkit.detectors.factory import DetectorFactory
|
|
29
30
|
|
|
30
31
|
# The detector types the interactive tuner can emit: the windowed statistical
|
|
@@ -68,12 +69,6 @@ class AppliedConfig:
|
|
|
68
69
|
preserved: tuple[str, ...] = ()
|
|
69
70
|
|
|
70
71
|
|
|
71
|
-
def _stamp(now: datetime | None = None) -> str:
|
|
72
|
-
"""UTC filesystem-safe timestamp (``20260624T101530Z``)."""
|
|
73
|
-
now = now or datetime.now(timezone.utc)
|
|
74
|
-
return now.strftime("%Y%m%dT%H%M%SZ")
|
|
75
|
-
|
|
76
|
-
|
|
77
72
|
def _apply_consecutive(body: dict[str, Any], consecutive: int) -> None:
|
|
78
73
|
"""Set ``consecutive_anomalies`` on the metric's first alerting config.
|
|
79
74
|
|
|
@@ -192,9 +187,9 @@ def apply_tuned_config(
|
|
|
192
187
|
if not isinstance(raw, dict):
|
|
193
188
|
raise ValueError(f"metric config is empty or malformed: {original_path}")
|
|
194
189
|
|
|
195
|
-
# Support the nested `metric: { ... }` form (
|
|
196
|
-
|
|
197
|
-
body: dict[str, Any] = raw
|
|
190
|
+
# Support the nested `metric: { ... }` form (shared unwrap; the body is a
|
|
191
|
+
# reference into `raw`, so edits below land inside the original document).
|
|
192
|
+
body: dict[str, Any] = unwrap_metric_mapping(raw)
|
|
198
193
|
|
|
199
194
|
existing = body.get("detectors")
|
|
200
195
|
merged: list[Any] = list(existing) if isinstance(existing, list) else []
|
|
@@ -227,11 +222,10 @@ def apply_tuned_config(
|
|
|
227
222
|
validated = MetricConfig.model_validate(body)
|
|
228
223
|
metric_name = validated.name
|
|
229
224
|
|
|
230
|
-
stamp =
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
archive_path =
|
|
234
|
-
archive_path.write_text(original_text, encoding="utf-8")
|
|
225
|
+
stamp = metric_stamp(now)
|
|
226
|
+
# Shared, collision-safe archive seam (config/metric_io.py) — a UI save and
|
|
227
|
+
# this Apply landing in the same UTC second keep both snapshots.
|
|
228
|
+
archive_path = archive_metric_text(project_root, metric_name, original_text, stamp=stamp)
|
|
235
229
|
|
|
236
230
|
try:
|
|
237
231
|
archive_rel = str(archive_path.relative_to(project_root))
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
A localhost web UI over the already-persisted ``_dtk_*`` tables: an overview of
|
|
4
4
|
every selected metric's alerting behavior (how often alerts fire, per metric /
|
|
5
5
|
per tag / per ``metrics/`` folder), a per-metric detail view (the existing HTML
|
|
6
|
-
report in an iframe),
|
|
6
|
+
report in an iframe), a pipeline control panel that drives the existing CLI
|
|
7
7
|
commands (``dtk run`` / ``dtk autotune`` / ``dtk unlock`` as subprocesses,
|
|
8
|
-
``dtk tune`` launched per metric)
|
|
8
|
+
``dtk tune`` launched per metric), and metric management — creating, editing
|
|
9
|
+
and deleting metric YAML files from a browser editor.
|
|
9
10
|
|
|
10
11
|
``dtk ui`` is a *superstructure over existing dtk commands*: the server itself
|
|
11
12
|
never runs the pipeline in-process and takes no pipeline lock — spawned ``dtk
|
|
@@ -20,6 +21,9 @@ numbers match what the pipeline would actually have alerted.
|
|
|
20
21
|
pipeline control panel.
|
|
21
22
|
- ``server.py`` — ``build_ui_server`` / ``serve_ui``: routes, token auth, the
|
|
22
23
|
single ``db_lock`` serializing DB-touching requests.
|
|
24
|
+
- ``metric_files.py`` — the metric-YAML create/update/delete seam: validate
|
|
25
|
+
before write, archive to ``metrics/.history/<metric>/`` before every
|
|
26
|
+
overwrite or delete.
|
|
23
27
|
- ``html.py`` — ``render_ui_html``: inlines the committed ``assets/ui.js``
|
|
24
28
|
renderer bundle + the boot payload into one self-contained page.
|
|
25
29
|
"""
|