detectkit 0.47.0__tar.gz → 0.49.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.47.0 → detectkit-0.49.0}/MANIFEST.in +1 -0
- {detectkit-0.47.0/detectkit.egg-info → detectkit-0.49.0}/PKG-INFO +1 -1
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/__init__.py +1 -1
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/CLAUDE.section.md +1 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/rules/cli.md +52 -3
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +11 -3
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/commands/run.py +48 -47
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/commands/tune.py +6 -1
- detectkit-0.49.0/detectkit/cli/commands/ui.py +97 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/main.py +54 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/config/validator.py +40 -4
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/internal_tables/_base.py +11 -2
- detectkit-0.49.0/detectkit/reporting/assets/report.js +80 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/reporting/builder.py +63 -28
- detectkit-0.49.0/detectkit/tuning/assets/tune.js +187 -0
- detectkit-0.49.0/detectkit/tuning/config_writer.py +253 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/tuning/payload.py +81 -7
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/tuning/server.py +47 -5
- detectkit-0.49.0/detectkit/ui/__init__.py +25 -0
- detectkit-0.49.0/detectkit/ui/assets/ui.js +243 -0
- detectkit-0.49.0/detectkit/ui/html.py +84 -0
- detectkit-0.49.0/detectkit/ui/jobs.py +334 -0
- detectkit-0.49.0/detectkit/ui/overview.py +475 -0
- detectkit-0.49.0/detectkit/ui/server.py +614 -0
- {detectkit-0.47.0 → detectkit-0.49.0/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit.egg-info/SOURCES.txt +7 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/pyproject.toml +2 -0
- detectkit-0.47.0/detectkit/reporting/assets/report.js +0 -80
- detectkit-0.47.0/detectkit/tuning/assets/tune.js +0 -187
- detectkit-0.47.0/detectkit/tuning/config_writer.py +0 -138
- {detectkit-0.47.0 → detectkit-0.49.0}/LICENSE +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/README.md +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/commands/osi.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/core/models.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/semantic/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/semantic/errors.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/semantic/exporter.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/semantic/importer.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/semantic/osi_model.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/semantic/query_gen.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/requirements.txt +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/setup.cfg +0 -0
- {detectkit-0.47.0 → detectkit-0.49.0}/setup.py +0 -0
|
@@ -5,6 +5,7 @@ recursive-include detectkit *.py
|
|
|
5
5
|
recursive-include detectkit/cli/assets *.md
|
|
6
6
|
recursive-include detectkit/reporting/assets *.js
|
|
7
7
|
recursive-include detectkit/tuning/assets *.js
|
|
8
|
+
recursive-include detectkit/ui/assets *.js
|
|
8
9
|
recursive-exclude tests *
|
|
9
10
|
recursive-exclude * __pycache__
|
|
10
11
|
recursive-exclude * *.pyc
|
|
@@ -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.49.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -40,6 +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 + driving the pipeline from the browser (`dtk ui`) | `.claude/rules/detectkit/cli.md` |
|
|
43
44
|
| OSI (Open Semantic Interchange) interop: import an OSI metric into a native metric / export back (`dtk osi`) | `.claude/rules/detectkit/cli.md` |
|
|
44
45
|
|
|
45
46
|
### Skills
|
|
@@ -12,6 +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
16
|
| `dtk test-alert <metric>` | Send a mock alert to the metric's channels |
|
|
16
17
|
| `dtk unlock --select <sel>` | Clear a stuck pipeline lock |
|
|
17
18
|
| `dtk clean --select <sel>` | Prune internal data that no longer matches the config |
|
|
@@ -97,7 +98,14 @@ anomalies**) and the **y = 0** toggle — stay visible in every mode. The chart
|
|
|
97
98
|
**zoomable** (scroll/drag + navigator strip) with a **"Points shown"** trim slider.
|
|
98
99
|
Clicking **Apply** writes the chosen
|
|
99
100
|
config back into the metric YAML **in place** (autotune, by contrast, writes a new
|
|
100
|
-
`__tuned_<id>.yml` and never edits the original).
|
|
101
|
+
`__tuned_<id>.yml` and never edits the original). Apply **merges** — it rewrites
|
|
102
|
+
only the detector(s) you tuned and keeps every **other** detector unchanged, so a
|
|
103
|
+
`manual_bounds` floor alongside a `mad` detector (and a `min_detectors: 2` alert)
|
|
104
|
+
survives a retune. If a metric configures **more than one detector**, a **Tuning
|
|
105
|
+
detector** picker in the Tune rail lets you choose which one to tune (the cockpit
|
|
106
|
+
shows one band at a time); switching re-seeds every knob from that detector, and
|
|
107
|
+
non-tunable detectors (`prophet`/`timesfm`) plus the ones you didn't touch are
|
|
108
|
+
listed as "preserved on Apply". Reads the metric's loaded
|
|
101
109
|
datapoints (run `dtk run --steps load` first if empty); the selector must resolve
|
|
102
110
|
to a single metric.
|
|
103
111
|
|
|
@@ -155,12 +163,53 @@ relative to zero.
|
|
|
155
163
|
|
|
156
164
|
Safe write-back: the config is validated before anything is written, the previous
|
|
157
165
|
YAML is archived under `metrics/.history/<metric>/`, and only then is the metric
|
|
158
|
-
overwritten
|
|
159
|
-
|
|
166
|
+
overwritten (the merge keeps your other detectors verbatim; the re-emitted header
|
|
167
|
+
names what was updated vs preserved). The `.history/` archive is **not** discovered
|
|
168
|
+
as a live metric, so a tuned metric never collides with its own snapshots as a
|
|
169
|
+
"duplicate metric name". Takes **no pipeline lock** (it only edits a config file);
|
|
170
|
+
re-run `dtk run` afterward to recompute detections under the new config.
|
|
160
171
|
`--no-serve` writes a static read-only preview HTML instead (no write-back —
|
|
161
172
|
**Save incidents** downloads the labels file); `--from` / `--to` bound the window;
|
|
162
173
|
`--no-open` prints the URL without opening a browser.
|
|
163
174
|
|
|
175
|
+
## `dtk ui`
|
|
176
|
+
|
|
177
|
+
A **project-wide** localhost cockpit over every selected metric's alerting
|
|
178
|
+
behavior — the live counterpart to a one-off `--report` file. Opens an
|
|
179
|
+
**overview** table (grouped by `metrics/` subfolder, filterable by tag) with,
|
|
180
|
+
per metric: alerts in the selected window + per-day rate + last alert,
|
|
181
|
+
no-data events, anomaly rate, data freshness, and a sparkline — all
|
|
182
|
+
**replayed** from stored detections through the same seam `dtk run --report`
|
|
183
|
+
uses, so the counts match what the pipeline would actually have alerted.
|
|
184
|
+
When `incidents/<metric>/` labels exist (from `dtk tune`'s Label/Review +
|
|
185
|
+
Save incidents), the row also shows recall / false-alert rate / reviewed,
|
|
186
|
+
matched the same way the `dtk tune` cockpit's metrics bar does; without
|
|
187
|
+
labels the row still shows frequency stats to eyeball.
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
dtk ui [-s/--select "*"] [--window 24h|7d|30d|90d|all] [--profile NAME] [--no-open]
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- `--select` — same selector syntax as `dtk run`; scopes which metrics the
|
|
194
|
+
overview and pipeline panel cover (default `*`, everything).
|
|
195
|
+
- `--window` — the preset selected when the page opens (default `30d`);
|
|
196
|
+
switch freely in the browser afterward.
|
|
197
|
+
- `--profile` — forwarded to the DB connection **and** to every subprocess
|
|
198
|
+
the pipeline panel spawns.
|
|
199
|
+
- `--no-open` — print the URL instead of opening a browser tab.
|
|
200
|
+
|
|
201
|
+
Clicking a metric's **Open** shows the existing self-contained HTML report
|
|
202
|
+
(the same one `--report` writes) in an overlay. A **pipeline panel** drives
|
|
203
|
+
the real CLI as subprocesses — `dtk run` (select/steps/from/to/force/
|
|
204
|
+
full-refresh), `dtk autotune`, `dtk unlock` — streaming their terminal
|
|
205
|
+
output live; **only one of these three runs at a time** (they'd contend for
|
|
206
|
+
the same pipeline lock anyway). **Tune** launches `dtk tune --select
|
|
207
|
+
<metric>` for a metric and opens its cockpit in a new tab — unlike
|
|
208
|
+
run/autotune/unlock, **multiple tune jobs run concurrently**, since each is
|
|
209
|
+
its own isolated, lock-free session. The `dtk ui` server itself takes **no
|
|
210
|
+
pipeline lock** and never mutates anything — every spawned command behaves
|
|
211
|
+
exactly as if typed into a terminal.
|
|
212
|
+
|
|
164
213
|
## `dtk test-alert <metric>`
|
|
165
214
|
|
|
166
215
|
Sends a mock alert (fake value/CI/severity) through the metric's configured
|
|
@@ -146,9 +146,17 @@ false-alert rate exceeds it (tuning-only; labeling stays optional).
|
|
|
146
146
|
When the user is happy, they click **Apply to metric**. detectkit then, in order:
|
|
147
147
|
**validates** the config (a bad/untunable config is rejected and **nothing is
|
|
148
148
|
written**), **archives** the current YAML verbatim under
|
|
149
|
-
`metrics/.history/<metric>/`, and **re-emits** the metric in place
|
|
150
|
-
detector (and
|
|
151
|
-
one). Applying ends the session; saving incidents
|
|
149
|
+
`metrics/.history/<metric>/`, and **re-emits** the metric in place, **merging** the
|
|
150
|
+
tuned detector(s) back in (and updating the first alerting block's
|
|
151
|
+
`consecutive_anomalies` if it has one). Applying ends the session; saving incidents
|
|
152
|
+
does not. **Merge, not replace:** only the detector(s) the user tuned are
|
|
153
|
+
rewritten; every other detector is kept **verbatim** — so a metric with a
|
|
154
|
+
`manual_bounds` floor alongside a `mad` detector (and a `min_detectors: 2` alert)
|
|
155
|
+
keeps both. When a metric has more than one detector, the Tune rail shows a
|
|
156
|
+
**Tuning detector** picker to choose which one to tune. If the user asks to tune a
|
|
157
|
+
metric that has several detectors, reassure them the others are preserved. (The
|
|
158
|
+
`.history/` archive is not discovered as a live metric, so it never causes a
|
|
159
|
+
"duplicate metric name" error.)
|
|
152
160
|
|
|
153
161
|
## Step 5 — Recompute under the new config
|
|
154
162
|
|
|
@@ -12,7 +12,11 @@ import click
|
|
|
12
12
|
from detectkit.config.metric_config import MetricConfig
|
|
13
13
|
from detectkit.config.profile import ProfilesConfig
|
|
14
14
|
from detectkit.config.project_config import ProjectConfig
|
|
15
|
-
from detectkit.config.validator import
|
|
15
|
+
from detectkit.config.validator import (
|
|
16
|
+
discover_metric_files,
|
|
17
|
+
is_discoverable_metric_file,
|
|
18
|
+
validate_metric_uniqueness,
|
|
19
|
+
)
|
|
16
20
|
from detectkit.database.internal_tables import InternalTablesManager
|
|
17
21
|
from detectkit.orchestration.error_dispatch import dispatch_project_error_alert
|
|
18
22
|
from detectkit.orchestration.task_manager import PipelineStep, TaskManager
|
|
@@ -433,17 +437,16 @@ def select_metrics(selector: str, project_root: Path) -> list[tuple[Path, Metric
|
|
|
433
437
|
if selector == "*":
|
|
434
438
|
# "all metrics" — search recursively so nested metrics are included
|
|
435
439
|
# (mirrors validate_project_metrics); a plain glob of "metrics/*"
|
|
436
|
-
# would only see the top level.
|
|
437
|
-
metric_paths =
|
|
440
|
+
# would only see the top level. Excludes the metrics/.history/ archive.
|
|
441
|
+
metric_paths = discover_metric_files(metrics_dir)
|
|
438
442
|
else:
|
|
439
443
|
pattern = selector if selector.startswith("metrics/") else f"metrics/{selector}"
|
|
440
444
|
# Keep only metric files: a bare glob also matches the `.gitkeep`
|
|
441
|
-
# stub created by `dtk init`, any other non-YAML files, and
|
|
442
|
-
#
|
|
445
|
+
# stub created by `dtk init`, any other non-YAML files, directories, and
|
|
446
|
+
# the metrics/.history/ archive dtk tune writes (whose snapshots keep the
|
|
447
|
+
# original name:) — all of which would crash the parser or collide on name.
|
|
443
448
|
metric_paths = [
|
|
444
|
-
p
|
|
445
|
-
for p in project_root.glob(pattern)
|
|
446
|
-
if p.is_file() and p.suffix in (".yml", ".yaml")
|
|
449
|
+
p for p in project_root.glob(pattern) if is_discoverable_metric_file(p, metrics_dir)
|
|
447
450
|
]
|
|
448
451
|
# Metric name selector
|
|
449
452
|
else:
|
|
@@ -485,26 +488,25 @@ def find_metrics_by_tag(metrics_dir: Path, tag: str) -> list[Path]:
|
|
|
485
488
|
|
|
486
489
|
matching_metrics = []
|
|
487
490
|
|
|
488
|
-
#
|
|
489
|
-
for
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
click.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
continue
|
|
491
|
+
# Live metric files only (excludes the metrics/.history/ archive).
|
|
492
|
+
for metric_file in discover_metric_files(metrics_dir):
|
|
493
|
+
try:
|
|
494
|
+
with open(metric_file) as f:
|
|
495
|
+
config = yaml.safe_load(f)
|
|
496
|
+
|
|
497
|
+
if config and "tags" in config:
|
|
498
|
+
if tag in config["tags"]:
|
|
499
|
+
matching_metrics.append(metric_file)
|
|
500
|
+
except Exception as e:
|
|
501
|
+
# Warn about unparseable files but continue searching
|
|
502
|
+
click.echo(
|
|
503
|
+
click.style(
|
|
504
|
+
f"Warning: Skipping {metric_file.relative_to(metrics_dir.parent)}: {e}",
|
|
505
|
+
fg="yellow",
|
|
506
|
+
),
|
|
507
|
+
err=True,
|
|
508
|
+
)
|
|
509
|
+
continue
|
|
508
510
|
|
|
509
511
|
return matching_metrics
|
|
510
512
|
|
|
@@ -522,25 +524,24 @@ def find_metric_by_name(metrics_dir: Path, name: str) -> Path | None:
|
|
|
522
524
|
"""
|
|
523
525
|
import yaml
|
|
524
526
|
|
|
525
|
-
#
|
|
526
|
-
for
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
click.
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
continue
|
|
527
|
+
# Live metric files only (excludes the metrics/.history/ archive).
|
|
528
|
+
for metric_file in discover_metric_files(metrics_dir):
|
|
529
|
+
try:
|
|
530
|
+
with open(metric_file) as f:
|
|
531
|
+
config = yaml.safe_load(f)
|
|
532
|
+
|
|
533
|
+
if config and config.get("name") == name:
|
|
534
|
+
return metric_file
|
|
535
|
+
except Exception as e:
|
|
536
|
+
# Warn about unparseable files but continue searching
|
|
537
|
+
click.echo(
|
|
538
|
+
click.style(
|
|
539
|
+
f"Warning: Skipping {metric_file.relative_to(metrics_dir.parent)}: {e}",
|
|
540
|
+
fg="yellow",
|
|
541
|
+
),
|
|
542
|
+
err=True,
|
|
543
|
+
)
|
|
544
|
+
continue
|
|
544
545
|
|
|
545
546
|
return None
|
|
546
547
|
|
|
@@ -153,6 +153,11 @@ def run_tune(
|
|
|
153
153
|
return False
|
|
154
154
|
|
|
155
155
|
click.echo(f" Archived previous config: {applied.archived.relative_to(project_root)}")
|
|
156
|
-
|
|
156
|
+
updated = ", ".join(applied.updated) or "detector"
|
|
157
|
+
echo_done(
|
|
158
|
+
f"{name}: applied tuned detector(s) [{updated}] → {applied.saved.relative_to(project_root)}"
|
|
159
|
+
)
|
|
160
|
+
if applied.preserved:
|
|
161
|
+
click.echo(f" Preserved the metric's other detector(s): {', '.join(applied.preserved)}")
|
|
157
162
|
click.echo(f" Re-run `dtk run --select {name}` to recompute detections under the new config.")
|
|
158
163
|
return True
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""``dtk ui`` — the project-level monitoring cockpit.
|
|
2
|
+
|
|
3
|
+
Opens a localhost web UI over the already-persisted ``_dtk_*`` tables: an
|
|
4
|
+
overview of every selected metric's alerting behavior, a per-metric detail
|
|
5
|
+
view (the existing HTML report in an iframe), and a pipeline control panel
|
|
6
|
+
that drives the real ``dtk run`` / ``dtk autotune`` / ``dtk unlock`` / ``dtk
|
|
7
|
+
tune`` commands as subprocesses. Like ``dtk tune`` it takes no pipeline lock —
|
|
8
|
+
it only reads the internal tables and spawns other ``dtk`` invocations, each
|
|
9
|
+
of which takes its own lock exactly as if run from the terminal.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from detectkit.cli.commands.run import find_project_root, select_metrics
|
|
17
|
+
from detectkit.config.profile import ProfilesConfig
|
|
18
|
+
from detectkit.config.project_config import ProjectConfig
|
|
19
|
+
from detectkit.database.internal_tables import InternalTablesManager
|
|
20
|
+
from detectkit.ui.overview import ALL_WINDOW_PRESETS
|
|
21
|
+
from detectkit.ui.server import serve_ui
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_ui(
|
|
25
|
+
*,
|
|
26
|
+
select: str = "*",
|
|
27
|
+
profile: str | None = None,
|
|
28
|
+
window: str = "30d",
|
|
29
|
+
no_open: bool = False,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Bootstrap the project and serve the cockpit until Ctrl-C."""
|
|
32
|
+
if window not in ALL_WINDOW_PRESETS:
|
|
33
|
+
allowed = ", ".join(sorted(ALL_WINDOW_PRESETS))
|
|
34
|
+
click.echo(
|
|
35
|
+
click.style(
|
|
36
|
+
f"Error: invalid --window '{window}'. Choose one of: {allowed}.",
|
|
37
|
+
fg="red",
|
|
38
|
+
bold=True,
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
raise SystemExit(1)
|
|
42
|
+
|
|
43
|
+
project_root = find_project_root()
|
|
44
|
+
if not project_root:
|
|
45
|
+
click.echo(click.style("Error: Not in a detectkit project directory!", fg="red", bold=True))
|
|
46
|
+
click.echo("Run 'dtk init <project_name>' to create a new project.")
|
|
47
|
+
raise SystemExit(1)
|
|
48
|
+
|
|
49
|
+
click.echo(f"Project root: {project_root}")
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
project_config = ProjectConfig.from_yaml_file(project_root / "detectkit_project.yml")
|
|
53
|
+
except Exception as exc:
|
|
54
|
+
click.echo(click.style(f"Error loading detectkit_project.yml: {exc}", fg="red", bold=True))
|
|
55
|
+
raise SystemExit(1) from exc
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
metrics = select_metrics(select, project_root)
|
|
59
|
+
except ValueError as exc:
|
|
60
|
+
click.echo(click.style(f"Error: {exc}", fg="red", bold=True))
|
|
61
|
+
raise SystemExit(1) from exc
|
|
62
|
+
if not metrics:
|
|
63
|
+
click.echo(click.style(f"No metrics found matching selector: {select}", fg="yellow"))
|
|
64
|
+
raise SystemExit(1)
|
|
65
|
+
metrics = sorted(metrics, key=lambda item: str(item[0]))
|
|
66
|
+
|
|
67
|
+
profiles_path = project_root / "profiles.yml"
|
|
68
|
+
if not profiles_path.exists():
|
|
69
|
+
click.echo(click.style("Error: profiles.yml not found!", fg="red", bold=True))
|
|
70
|
+
click.echo(f"Expected at: {profiles_path}")
|
|
71
|
+
raise SystemExit(1)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
profiles_config = ProfilesConfig.from_yaml(profiles_path)
|
|
75
|
+
db_manager = profiles_config.create_manager(profile)
|
|
76
|
+
internal_manager = InternalTablesManager(db_manager)
|
|
77
|
+
internal_manager.ensure_tables()
|
|
78
|
+
except Exception as exc:
|
|
79
|
+
click.echo(click.style(f"Error connecting to the database: {exc}", fg="red", bold=True))
|
|
80
|
+
raise SystemExit(1) from exc
|
|
81
|
+
|
|
82
|
+
click.echo(click.style(f"detectkit UI: {project_config.name}", fg="cyan", bold=True))
|
|
83
|
+
click.echo(f" {len(metrics)} metric(s) covered by --select {select} (window: {window})")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
serve_ui(
|
|
87
|
+
project_config=project_config,
|
|
88
|
+
project_root=project_root,
|
|
89
|
+
metrics=metrics,
|
|
90
|
+
internal_manager=internal_manager,
|
|
91
|
+
initial_window=window,
|
|
92
|
+
profile=profile,
|
|
93
|
+
echo=click.echo,
|
|
94
|
+
open_browser=not no_open,
|
|
95
|
+
)
|
|
96
|
+
except KeyboardInterrupt:
|
|
97
|
+
pass
|
|
@@ -307,6 +307,60 @@ def autotune(
|
|
|
307
307
|
)
|
|
308
308
|
|
|
309
309
|
|
|
310
|
+
@cli.command()
|
|
311
|
+
@click.option(
|
|
312
|
+
"--select",
|
|
313
|
+
"-s",
|
|
314
|
+
default="*",
|
|
315
|
+
show_default=True,
|
|
316
|
+
help="Selector for the metrics the UI covers (metric name, path, or tag)",
|
|
317
|
+
)
|
|
318
|
+
@click.option(
|
|
319
|
+
"--profile",
|
|
320
|
+
help="Profile to use (default: from project config)",
|
|
321
|
+
)
|
|
322
|
+
@click.option(
|
|
323
|
+
"--window",
|
|
324
|
+
default="30d",
|
|
325
|
+
show_default=True,
|
|
326
|
+
help="Initial window preset: 24h, 7d, 30d, 90d, or all",
|
|
327
|
+
)
|
|
328
|
+
@click.option(
|
|
329
|
+
"--no-open",
|
|
330
|
+
is_flag=True,
|
|
331
|
+
help="Don't auto-open the browser (just print the local URL)",
|
|
332
|
+
)
|
|
333
|
+
def ui(select: str, profile: str, window: str, no_open: bool) -> None:
|
|
334
|
+
"""
|
|
335
|
+
Open the project-level monitoring cockpit.
|
|
336
|
+
|
|
337
|
+
A localhost web UI over the already-persisted _dtk_* tables: an overview
|
|
338
|
+
of every selected metric's alerting behavior (how often alerts fire, per
|
|
339
|
+
metric / per tag / per metrics/ folder), a per-metric detail view (the
|
|
340
|
+
existing HTML report in an iframe), and a pipeline control panel that runs
|
|
341
|
+
`dtk run` / `dtk autotune` / `dtk unlock` / `dtk tune` as subprocesses —
|
|
342
|
+
exactly as if typed at a terminal. Alert counts are replayed from stored
|
|
343
|
+
detections, so the numbers match what the pipeline would actually have
|
|
344
|
+
alerted.
|
|
345
|
+
|
|
346
|
+
Takes no pipeline lock itself; spawned commands take their own lock.
|
|
347
|
+
Selector semantics match `dtk run`.
|
|
348
|
+
|
|
349
|
+
Examples:
|
|
350
|
+
# Cover every metric, starting on the last 30 days
|
|
351
|
+
dtk ui
|
|
352
|
+
|
|
353
|
+
# Only metrics tagged critical, starting on the last 7 days
|
|
354
|
+
dtk ui --select "tag:critical" --window 7d
|
|
355
|
+
|
|
356
|
+
# Don't auto-open the browser
|
|
357
|
+
dtk ui --no-open
|
|
358
|
+
"""
|
|
359
|
+
from detectkit.cli.commands.ui import run_ui
|
|
360
|
+
|
|
361
|
+
run_ui(select=select, profile=profile, window=window, no_open=no_open)
|
|
362
|
+
|
|
363
|
+
|
|
310
364
|
@cli.command()
|
|
311
365
|
@click.option(
|
|
312
366
|
"--select",
|
|
@@ -10,6 +10,44 @@ from pathlib import Path
|
|
|
10
10
|
from detectkit.config.metric_config import MetricConfig
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def is_discoverable_metric_file(path: Path, metrics_dir: Path) -> bool:
|
|
14
|
+
"""True for a *live* metric YAML — excludes the ``metrics/.history/`` archive.
|
|
15
|
+
|
|
16
|
+
``dtk tune`` archives the previous metric config under
|
|
17
|
+
``metrics/.history/<metric>/`` before writing the tuned version in place. Those
|
|
18
|
+
snapshots keep the original ``name:``, so discovering them as live metrics would
|
|
19
|
+
(a) flag every tuned metric as a duplicate-name conflict and (b) run stale
|
|
20
|
+
configs. Python's ``pathlib`` glob traverses hidden directories (unlike shell
|
|
21
|
+
globbing), so they must be filtered out explicitly. Any hidden path component
|
|
22
|
+
(a leading dot) *under* ``metrics/`` is skipped — this covers ``.history`` and
|
|
23
|
+
any editor/VCS scratch dir a user drops there, while a project rooted under a
|
|
24
|
+
dot-directory (checked only below ``metrics/``) is unaffected.
|
|
25
|
+
"""
|
|
26
|
+
if not path.is_file() or path.suffix not in (".yml", ".yaml"):
|
|
27
|
+
return False
|
|
28
|
+
try:
|
|
29
|
+
parts = path.relative_to(metrics_dir).parts
|
|
30
|
+
except ValueError:
|
|
31
|
+
parts = path.parts
|
|
32
|
+
return not any(part.startswith(".") for part in parts)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def discover_metric_files(metrics_dir: Path) -> list[Path]:
|
|
36
|
+
"""All live metric YAMLs under ``metrics/`` (recursive), excluding the ``.history`` archive.
|
|
37
|
+
|
|
38
|
+
The single discovery seam shared by project validation and CLI metric selection,
|
|
39
|
+
so the ``.history`` exclusion can't drift between them. Returns a sorted list for
|
|
40
|
+
deterministic ordering.
|
|
41
|
+
"""
|
|
42
|
+
files = [
|
|
43
|
+
p
|
|
44
|
+
for pattern in ("**/*.yml", "**/*.yaml")
|
|
45
|
+
for p in metrics_dir.glob(pattern)
|
|
46
|
+
if is_discoverable_metric_file(p, metrics_dir)
|
|
47
|
+
]
|
|
48
|
+
return sorted(set(files))
|
|
49
|
+
|
|
50
|
+
|
|
13
51
|
def validate_metric_uniqueness(metric_paths: list[Path]) -> list[tuple[Path, MetricConfig]]:
|
|
14
52
|
"""
|
|
15
53
|
Load all metrics and validate that metric names are unique.
|
|
@@ -106,10 +144,8 @@ def validate_project_metrics(project_root: Path) -> list[tuple[Path, MetricConfi
|
|
|
106
144
|
f" your_metric.yml\n"
|
|
107
145
|
)
|
|
108
146
|
|
|
109
|
-
# Find all metric files recursively
|
|
110
|
-
metric_paths =
|
|
111
|
-
for pattern in ["**/*.yml", "**/*.yaml"]:
|
|
112
|
-
metric_paths.extend(metrics_dir.glob(pattern))
|
|
147
|
+
# Find all metric files recursively (excluding the metrics/.history/ archive).
|
|
148
|
+
metric_paths = discover_metric_files(metrics_dir)
|
|
113
149
|
|
|
114
150
|
if not metric_paths:
|
|
115
151
|
raise ValueError(
|
|
@@ -9,7 +9,7 @@ look for those helpers.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
from datetime import datetime
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
13
|
|
|
14
14
|
from detectkit.database.manager import BaseDatabaseManager
|
|
15
15
|
|
|
@@ -24,12 +24,19 @@ class _InternalTablesBase:
|
|
|
24
24
|
|
|
25
25
|
@staticmethod
|
|
26
26
|
def _normalize_max_timestamp(value: datetime | None) -> datetime | None:
|
|
27
|
-
"""Treat the Unix epoch sentinel as a missing value.
|
|
27
|
+
"""Treat the Unix epoch sentinel as a missing value; return naive UTC.
|
|
28
28
|
|
|
29
29
|
ClickHouse's ``max(timestamp)`` over an empty selection returns
|
|
30
30
|
``1970-01-01 00:00:00`` instead of NULL. Without normalisation,
|
|
31
31
|
idempotency checks would think we already processed everything up
|
|
32
32
|
to 1970 and refuse to do work.
|
|
33
|
+
|
|
34
|
+
The driver may also hand back an *aware* datetime (clickhouse-driver
|
|
35
|
+
attaches the server column's timezone), while everything in-memory in
|
|
36
|
+
detectkit is naive UTC — mixing the two raises ``TypeError`` on the
|
|
37
|
+
first comparison/subtraction. Cursor reads are normalized to naive
|
|
38
|
+
UTC here, at the single seam every reader shares, instead of at each
|
|
39
|
+
call site.
|
|
33
40
|
"""
|
|
34
41
|
if value is None:
|
|
35
42
|
return None
|
|
@@ -38,4 +45,6 @@ class _InternalTablesBase:
|
|
|
38
45
|
epoch = epoch.replace(tzinfo=value.tzinfo)
|
|
39
46
|
if value == epoch:
|
|
40
47
|
return None
|
|
48
|
+
if value.tzinfo is not None:
|
|
49
|
+
return value.astimezone(timezone.utc).replace(tzinfo=None)
|
|
41
50
|
return value
|