detectkit 0.43.0__tar.gz → 0.44.1__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.43.0/detectkit.egg-info → detectkit-0.44.1}/PKG-INFO +1 -1
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/__init__.py +1 -1
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/autotuner.py +30 -2
- detectkit-0.44.1/detectkit/cli/_output.py +112 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/assets/claude/rules/alerting.md +3 -5
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/assets/claude/rules/autotune.md +6 -4
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/assets/claude/rules/cli.md +6 -4
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/assets/claude/rules/overview.md +1 -2
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/assets/claude/rules/project.md +1 -2
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/commands/autotune.py +11 -30
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/tuning/assets/tune.js +3 -3
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/tuning/server.py +128 -3
- {detectkit-0.43.0 → detectkit-0.44.1/detectkit.egg-info}/PKG-INFO +1 -1
- detectkit-0.43.0/detectkit/cli/_output.py +0 -50
- {detectkit-0.43.0 → detectkit-0.44.1}/LICENSE +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/MANIFEST.in +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/README.md +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/result.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/cli/main.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/config/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/config/profile.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/config/project_config.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/config/validator.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/core/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/core/interval.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/core/models.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/manager.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/database/tables.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/detectors/base.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/tuning/html.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/tuning/payload.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit/utils/stats.py +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/pyproject.toml +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/requirements.txt +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/setup.cfg +0 -0
- {detectkit-0.43.0 → detectkit-0.44.1}/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.44.1"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -6,7 +6,9 @@ command handles loading, persistence, config emission and candidate cleanup.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import contextlib
|
|
10
|
+
import logging
|
|
11
|
+
from collections.abc import Callable, Iterator
|
|
10
12
|
from datetime import datetime, timedelta
|
|
11
13
|
from typing import Any
|
|
12
14
|
|
|
@@ -27,6 +29,31 @@ from detectkit.detectors.factory import DetectorFactory
|
|
|
27
29
|
|
|
28
30
|
_ALERT_WINDOW_GRID = (1, 2, 3, 4, 5)
|
|
29
31
|
|
|
32
|
+
# The windowed detectors emit a one-time "seasonality group can't fill this
|
|
33
|
+
# window → falls back to global" warning per *instance*. A tune builds dozens of
|
|
34
|
+
# throwaway candidate detectors, so that warning would flood the terminal and
|
|
35
|
+
# bury the structured decision log. The engine already surfaces an under-fill of
|
|
36
|
+
# the *chosen* seasonality as a `window` advisory in its own log, so the raw
|
|
37
|
+
# per-candidate warnings are pure noise here.
|
|
38
|
+
_WINDOWED_LOGGER = "detectkit.detectors.statistical._windowed"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@contextlib.contextmanager
|
|
42
|
+
def _quiet_per_candidate_warnings() -> Iterator[None]:
|
|
43
|
+
"""Silence the windowed detectors' per-instance under-fill warning for a tune.
|
|
44
|
+
|
|
45
|
+
Scoped + restored, so a real ``dtk run`` (which builds one detector and *does*
|
|
46
|
+
want the warning) is unaffected — only the candidate sweep inside the engine
|
|
47
|
+
is quieted.
|
|
48
|
+
"""
|
|
49
|
+
logger = logging.getLogger(_WINDOWED_LOGGER)
|
|
50
|
+
prev_level = logger.level
|
|
51
|
+
logger.setLevel(logging.ERROR)
|
|
52
|
+
try:
|
|
53
|
+
yield
|
|
54
|
+
finally:
|
|
55
|
+
logger.setLevel(prev_level)
|
|
56
|
+
|
|
30
57
|
|
|
31
58
|
def _ts_to_dt(ts64: np.datetime64) -> datetime:
|
|
32
59
|
ms = int(ts64.astype("datetime64[ms]").astype(np.int64))
|
|
@@ -196,4 +223,5 @@ def run_autotune_engine(
|
|
|
196
223
|
settings=settings,
|
|
197
224
|
on_stage=on_stage,
|
|
198
225
|
)
|
|
199
|
-
|
|
226
|
+
with _quiet_per_candidate_warnings():
|
|
227
|
+
return tuner.tune()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Shared CLI output helpers so every command renders in one house style.
|
|
2
|
+
|
|
3
|
+
Mirrors the load → detect → alert pipeline's tree look (``┌─ / │ / └─``) used by
|
|
4
|
+
``dtk run`` so the maintenance commands (``dtk clean``, ``dtk unlock``) match it
|
|
5
|
+
instead of each inventing its own formatting.
|
|
6
|
+
|
|
7
|
+
House conventions:
|
|
8
|
+
- A metric *with* something to report is a tree: a cyan-bold ``┌─ <name>``
|
|
9
|
+
header followed by one child line per item (``│ `` for all but the last,
|
|
10
|
+
``└─ `` for the last).
|
|
11
|
+
- A metric with *nothing* to do is a single ``•`` line.
|
|
12
|
+
- A per-metric error is a red ``✗`` line (to stderr).
|
|
13
|
+
- The final summary is a cyan-bold ``Done. …`` line.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
# Autotune-engine stage name → display title for the streamed run-log tree.
|
|
23
|
+
# Most stages render as ``stage.upper()``; only the two underscored names need a
|
|
24
|
+
# space so the header reads cleanly (``DETECTOR SELECT`` / ``GRID SEARCH``).
|
|
25
|
+
AUTOTUNE_STAGE_TITLES = {
|
|
26
|
+
"labels": "LABELS",
|
|
27
|
+
"seasonality": "SEASONALITY",
|
|
28
|
+
"detector_select": "DETECTOR SELECT",
|
|
29
|
+
"grid_search": "GRID SEARCH",
|
|
30
|
+
"window": "WINDOW",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def echo_block(
|
|
35
|
+
title: str,
|
|
36
|
+
children: list[str],
|
|
37
|
+
*,
|
|
38
|
+
warnings: list[str] | None = None,
|
|
39
|
+
echo: Callable[[str], None] = click.echo,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Print a cyan-bold ``┌─ title`` header with ``│``/``└─`` child lines.
|
|
42
|
+
|
|
43
|
+
The injectable core of the house tree style (``dtk run``'s load/detect/alert
|
|
44
|
+
blocks): ``warnings`` render as yellow ``│`` continuation lines above the
|
|
45
|
+
children, the last child gets the ``└─`` elbow. ``echo`` defaults to
|
|
46
|
+
``click.echo`` but can be any line sink, so the tune server can stream the
|
|
47
|
+
same blocks through its own output callback. ``children`` must be non-empty.
|
|
48
|
+
"""
|
|
49
|
+
echo(click.style(f" ┌─ {title}", fg="cyan", bold=True))
|
|
50
|
+
for warning in warnings or []:
|
|
51
|
+
echo(click.style(f" │ ⚠ {warning}", fg="yellow", bold=True))
|
|
52
|
+
last = len(children) - 1
|
|
53
|
+
for i, child in enumerate(children):
|
|
54
|
+
prefix = " └─ " if i == last else " │ "
|
|
55
|
+
echo(f"{prefix}{child}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def echo_tree(name: str, children: list[str], *, warnings: list[str] | None = None) -> None:
|
|
59
|
+
"""Print a ``┌─ name`` header with ``│``/``└─`` child lines.
|
|
60
|
+
|
|
61
|
+
``warnings`` (if any) are rendered as yellow ``│`` continuation lines above
|
|
62
|
+
the children. ``children`` must be non-empty (a metric with no items should
|
|
63
|
+
use :func:`echo_noop` instead). Thin wrapper over :func:`echo_block` (the
|
|
64
|
+
CLI-default ``click.echo`` sink).
|
|
65
|
+
"""
|
|
66
|
+
echo_block(name, children, warnings=warnings)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class StageLogRenderer:
|
|
70
|
+
"""Stream ``(stage, line)`` engine progress as the run-log tree.
|
|
71
|
+
|
|
72
|
+
Opens a cyan-bold ``┌─ TITLE`` header the first time each stage appears and
|
|
73
|
+
prints every subsequent line for that stage as a ``│ `` child — the same
|
|
74
|
+
look as ``dtk run``'s load/detect/alert blocks. ``titles`` maps an engine
|
|
75
|
+
stage name to its display title (unmapped names fall back to ``upper()``);
|
|
76
|
+
``echo`` is injectable so one renderer drives both the CLI (``click.echo``)
|
|
77
|
+
and the ``dtk tune`` server (its own output callback). Build a fresh renderer
|
|
78
|
+
per run so the first stage re-opens its header.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
titles: dict[str, str] | None = None,
|
|
85
|
+
echo: Callable[[str], None] = click.echo,
|
|
86
|
+
) -> None:
|
|
87
|
+
self._open: str | None = None
|
|
88
|
+
self._titles = titles or {}
|
|
89
|
+
self._echo = echo
|
|
90
|
+
|
|
91
|
+
def __call__(self, stage: str, line: str) -> None:
|
|
92
|
+
if self._open != stage:
|
|
93
|
+
title = self._titles.get(stage, stage.upper())
|
|
94
|
+
self._echo(click.style(f" ┌─ {title}", fg="cyan", bold=True))
|
|
95
|
+
self._open = stage
|
|
96
|
+
self._echo(f" │ {line}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def echo_noop(name: str, reason: str) -> None:
|
|
100
|
+
"""A metric with nothing to do — a single ``•`` line."""
|
|
101
|
+
click.echo(f" • {name}: {reason}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def echo_error(name: str, message: str) -> None:
|
|
105
|
+
"""A per-metric failure — a red ``✗`` line on stderr."""
|
|
106
|
+
click.echo(click.style(f" ✗ {name}: {message}", fg="red"), err=True)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def echo_done(summary: str) -> None:
|
|
110
|
+
"""The closing ``Done. …`` summary (cyan, bold), preceded by a blank line."""
|
|
111
|
+
click.echo()
|
|
112
|
+
click.echo(click.style(f"Done. {summary}", fg="cyan", bold=True))
|
|
@@ -164,8 +164,7 @@ config needed. Control it project-wide with `alert_help_url` in
|
|
|
164
164
|
- **a URL string** → your own runbook/wiki page instead
|
|
165
165
|
- **`false`** → hide the link entirely
|
|
166
166
|
|
|
167
|
-
Per-channel rendering (defaults only; resolved
|
|
168
|
-
`ProjectConfig.resolve_alert_help_url`):
|
|
167
|
+
Per-channel rendering (defaults only; the resolved help URL is rendered per channel as follows):
|
|
169
168
|
|
|
170
169
|
- **Slack / Mattermost / generic webhook** — a clickable `How to read this alert`
|
|
171
170
|
label in the compact `Links` field (alongside `Dashboard` + any extra links),
|
|
@@ -185,7 +184,7 @@ mirrors `{dashboard_url}` / `{dashboard_line}`. See the template table below.
|
|
|
185
184
|
|
|
186
185
|
With no custom `template`, each channel renders a structured, branded message
|
|
187
186
|
(alert-centric: the rule that fired leads, the anomaly value is evidence). The
|
|
188
|
-
shared value computation lives in one place
|
|
187
|
+
shared value computation lives in one place,
|
|
189
188
|
so templates and native rendering stay consistent. Every alert title/headline
|
|
190
189
|
leads with a colored **status circle** — 🔴 anomaly, 🟢 recovery, 🟡 no-data,
|
|
191
190
|
🔵 pipeline error — so the status reads from color alone. It also leads with the
|
|
@@ -241,8 +240,7 @@ Latest reading** fields bound the span. Labels are self-describing so the onset
|
|
|
241
240
|
isn't misread as the alert-fire moment: **Anomaly began** is the resolved onset,
|
|
242
241
|
**not** when the alert fired. Recovery shows the fuller **began → fired →
|
|
243
242
|
recovered** timeline (`Incident lasted …`), where **Alert fired** =
|
|
244
|
-
`onset + (consecutive_required − 1) × interval` (
|
|
245
|
-
exposed as `{fired_display}`, omitted when the run is capped). The true
|
|
243
|
+
`onset + (consecutive_required − 1) × interval` (exposed as `{fired_display}`, omitted when the run is capped). The true
|
|
246
244
|
streak/onset is resolved only when an alert fires/clears (a bounded lookback over
|
|
247
245
|
the detection history; a run older than the window shows `over …`), so the hot
|
|
248
246
|
no-alert path stays cheap. Exposed to templates as `{anomaly_lead}` /
|
|
@@ -21,10 +21,12 @@ same windowed detectors and `detector_id` identity). The fastest path is the
|
|
|
21
21
|
> **You can also run this engine *inside* `dtk tune`** — its **Autotune** mode runs
|
|
22
22
|
> the same search **server-side** (using the incidents you've marked as ground
|
|
23
23
|
> truth), re-seeds the knobs with the winner, and lets you **Apply** in place. That's
|
|
24
|
-
> the same computation as this command
|
|
25
|
-
>
|
|
26
|
-
>
|
|
27
|
-
> run
|
|
24
|
+
> the same computation as this command, with two differences: it tunes on the
|
|
25
|
+
> **window currently shown** in the cockpit (the **Points shown** trim — the series
|
|
26
|
+
> you see and score) rather than the full capped history, and it edits the metric YAML
|
|
27
|
+
> in place (advisory — no run record / `__tuned_<id>.yml` / persisted detections)
|
|
28
|
+
> rather than emitting a new tuned file. Reach for `dtk autotune` when you want the
|
|
29
|
+
> audited run + tuned file + persisted winner detections.
|
|
28
30
|
|
|
29
31
|
## What it searches
|
|
30
32
|
|
|
@@ -109,10 +109,12 @@ anomalies** — loop a cloud of anomaly dots, each consecutive run, gaps bridged
|
|
|
109
109
|
`consecutive_anomalies`, becomes one span — or **Threshold capture** every span past
|
|
110
110
|
a horizontal line, each widened to a full interval so the alert lands inside; the
|
|
111
111
|
painted window saves as `capture_windows`), and **Autotune** (**Run autotune**
|
|
112
|
-
launches the **real** `dtk autotune` engine **server-side** over the
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
launches the **real** `dtk autotune` engine **server-side** over the **window
|
|
113
|
+
currently shown** (the **Points shown** trim — the same series you see and score, not
|
|
114
|
+
the full history), using your marked incidents as ground truth, then **re-seeds every
|
|
115
|
+
knob** with the winner and shows the score + decision log; the band leads like Tune;
|
|
116
|
+
each run also streams a structured `LABELS → … → RESULT` log to the terminal, like
|
|
117
|
+
`dtk run`). The Autotune mode is **advisory** — it computes + re-seeds only and persists nothing (no
|
|
116
118
|
run record / `__tuned_<id>.yml` / detections, so `dtk tune` stays lock-free); review
|
|
117
119
|
the band and **Apply** to write it back. It honours the metric's `autotune:` block,
|
|
118
120
|
is supervised when incidents are marked (also picks `consecutive_anomalies`) else
|
|
@@ -114,6 +114,5 @@ series, watch the band recompute live, then write the config back into the metri
|
|
|
114
114
|
|
|
115
115
|
## Authoritative sources
|
|
116
116
|
|
|
117
|
-
The
|
|
118
|
-
<https://dtk.pipelab.dev>) and the `CHANGELOG.md` are authoritative for
|
|
117
|
+
The detectkit docs at <https://dtk.pipelab.dev> and the project changelog are authoritative for
|
|
119
118
|
behavior. These rule files summarize them for the installed version.
|
|
@@ -45,8 +45,7 @@ overrides it; unset → a built-in `0.5`. Tuning-only — it never touches the p
|
|
|
45
45
|
### `alert_help_url` — "How to read this alert" link
|
|
46
46
|
|
|
47
47
|
Every default-rendered alert on every channel carries a `How to read this alert`
|
|
48
|
-
link for non-operator stakeholders. Tri-state
|
|
49
|
-
`ProjectConfig.resolve_alert_help_url`:
|
|
48
|
+
link for non-operator stakeholders. Tri-state:
|
|
50
49
|
|
|
51
50
|
- **unset / null** (default) → the official detectkit guide
|
|
52
51
|
(`https://dtk.pipelab.dev/guides/reading-alerts/`).
|
|
@@ -31,7 +31,14 @@ from detectkit.autotune.labels import (
|
|
|
31
31
|
parse_incident_labels,
|
|
32
32
|
)
|
|
33
33
|
from detectkit.autotune.runner import build_settings, cap_history, resolve_scoring
|
|
34
|
-
from detectkit.cli._output import
|
|
34
|
+
from detectkit.cli._output import (
|
|
35
|
+
AUTOTUNE_STAGE_TITLES,
|
|
36
|
+
StageLogRenderer,
|
|
37
|
+
echo_block,
|
|
38
|
+
echo_done,
|
|
39
|
+
echo_error,
|
|
40
|
+
echo_noop,
|
|
41
|
+
)
|
|
35
42
|
from detectkit.cli.commands.run import find_project_root, parse_date, select_metrics
|
|
36
43
|
from detectkit.config.metric_config import AutoTuneConfig, MetricConfig
|
|
37
44
|
from detectkit.config.profile import ProfilesConfig
|
|
@@ -41,28 +48,6 @@ from detectkit.detectors.base import DetectionResult
|
|
|
41
48
|
from detectkit.detectors.factory import DetectorFactory
|
|
42
49
|
from detectkit.utils.json_utils import json_dumps_sorted
|
|
43
50
|
|
|
44
|
-
_STAGE_TITLES = {
|
|
45
|
-
"labels": "LABELS",
|
|
46
|
-
"seasonality": "SEASONALITY",
|
|
47
|
-
"detector_select": "DETECTOR SELECT",
|
|
48
|
-
"grid_search": "GRID SEARCH",
|
|
49
|
-
"window": "WINDOW",
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class _StageRenderer:
|
|
54
|
-
"""Streams engine progress as the run-log tree (header per stage + │ lines)."""
|
|
55
|
-
|
|
56
|
-
def __init__(self) -> None:
|
|
57
|
-
self._open: str | None = None
|
|
58
|
-
|
|
59
|
-
def __call__(self, stage: str, line: str) -> None:
|
|
60
|
-
if self._open != stage:
|
|
61
|
-
title = _STAGE_TITLES.get(stage, stage.upper())
|
|
62
|
-
click.echo(click.style(f" ┌─ {title}", fg="cyan", bold=True))
|
|
63
|
-
self._open = stage
|
|
64
|
-
click.echo(f" │ {line}")
|
|
65
|
-
|
|
66
51
|
|
|
67
52
|
def _results_to_batch(results: list[DetectionResult]) -> dict[str, np.ndarray]:
|
|
68
53
|
"""Convert detect() output into the ``save_detections`` batch shape."""
|
|
@@ -358,7 +343,7 @@ def _tune_one(
|
|
|
358
343
|
ground_truth=ground_truth,
|
|
359
344
|
interval_seconds=interval_seconds,
|
|
360
345
|
settings=settings,
|
|
361
|
-
on_stage=
|
|
346
|
+
on_stage=StageLogRenderer(titles=AUTOTUNE_STAGE_TITLES),
|
|
362
347
|
)
|
|
363
348
|
|
|
364
349
|
run_id = compute_run_id(result)
|
|
@@ -434,9 +419,7 @@ def _finalize(
|
|
|
434
419
|
|
|
435
420
|
if dry_run:
|
|
436
421
|
children.append("dry-run: no config written, no detections persisted")
|
|
437
|
-
|
|
438
|
-
for i, child in enumerate(children):
|
|
439
|
-
click.echo(f" {'└─' if i == len(children) - 1 else '│ '} {child}")
|
|
422
|
+
echo_block("RESULT", children)
|
|
440
423
|
return
|
|
441
424
|
|
|
442
425
|
# Persist the run record (the audit trail).
|
|
@@ -518,9 +501,7 @@ def _finalize(
|
|
|
518
501
|
f"persisted winner, pruned {pruned} superseded run(s)"
|
|
519
502
|
)
|
|
520
503
|
children.append(f"Re-run with: dtk run --select {name}__tuned_{run_id}")
|
|
521
|
-
|
|
522
|
-
for i, child in enumerate(children):
|
|
523
|
-
click.echo(f" {'└─' if i == len(children) - 1 else '│ '} {child}")
|
|
504
|
+
echo_block("RESULT", children)
|
|
524
505
|
|
|
525
506
|
|
|
526
507
|
def _prune_prior_winners(
|