detectkit 0.60.0__tar.gz → 0.62.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.60.0/detectkit.egg-info → detectkit-0.62.0}/PKG-INFO +19 -1
- {detectkit-0.60.0 → detectkit-0.62.0}/README.md +15 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/__init__.py +1 -1
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/cli.md +35 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/metrics.md +14 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/project.md +21 -0
- detectkit-0.62.0/detectkit/cli/commands/mcp.py +31 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/commands/run.py +135 -68
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/main.py +56 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/config/metric_config.py +36 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/config/profile.py +22 -3
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/config/project_config.py +15 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/_sql_manager.py +9 -2
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/clickhouse_manager.py +11 -5
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/duckdb_manager.py +25 -1
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/mysql_manager.py +5 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/postgres_manager.py +5 -0
- detectkit-0.62.0/detectkit/loaders/errors.py +29 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/loaders/metric_loader.py +30 -4
- detectkit-0.62.0/detectkit/mcp/__init__.py +47 -0
- detectkit-0.62.0/detectkit/mcp/context.py +329 -0
- detectkit-0.62.0/detectkit/mcp/errors.py +27 -0
- detectkit-0.62.0/detectkit/mcp/serialize.py +90 -0
- detectkit-0.62.0/detectkit/mcp/server.py +263 -0
- detectkit-0.62.0/detectkit/mcp/tools.py +502 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/_base.py +80 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/_load_step.py +6 -1
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/utils/datetime_utils.py +11 -2
- {detectkit-0.60.0 → detectkit-0.62.0/detectkit.egg-info}/PKG-INFO +19 -1
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit.egg-info/SOURCES.txt +8 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit.egg-info/requires.txt +4 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/pyproject.toml +7 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/LICENSE +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/MANIFEST.in +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/discord.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/googlechat.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/ntfy.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/teams.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/axis_spec.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/commands/osi.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/commands/ui.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/config/metric_io.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/core/models.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/detectors/statistical/autoreg.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/semantic/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/semantic/errors.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/semantic/exporter.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/semantic/importer.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/semantic/osi_model.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/semantic/query_gen.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/tuning/assets/tune.js +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/tuning/payload.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/ui/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/ui/assets/ui.js +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/ui/html.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/ui/jobs.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/ui/metric_files.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/ui/overview.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/ui/server.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/requirements.txt +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/setup.cfg +0 -0
- {detectkit-0.60.0 → detectkit-0.62.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: detectkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.62.0
|
|
4
4
|
Summary: Metric monitoring with automatic anomaly detection
|
|
5
5
|
Author: detectkit team
|
|
6
6
|
License: MIT
|
|
@@ -54,6 +54,8 @@ Requires-Dist: prophet>=1.1.0; extra == "advanced-detectors"
|
|
|
54
54
|
Requires-Dist: timesfm>=0.1.0; extra == "advanced-detectors"
|
|
55
55
|
Provides-Extra: osi
|
|
56
56
|
Requires-Dist: sqlglot>=20.0; extra == "osi"
|
|
57
|
+
Provides-Extra: mcp
|
|
58
|
+
Requires-Dist: mcp<2,>=1.27; extra == "mcp"
|
|
57
59
|
Provides-Extra: all
|
|
58
60
|
Requires-Dist: clickhouse-driver>=0.2.0; extra == "all"
|
|
59
61
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
@@ -62,6 +64,7 @@ Requires-Dist: duckdb>=1.1; extra == "all"
|
|
|
62
64
|
Requires-Dist: prophet>=1.1.0; extra == "all"
|
|
63
65
|
Requires-Dist: timesfm>=0.1.0; extra == "all"
|
|
64
66
|
Requires-Dist: sqlglot>=20.0; extra == "all"
|
|
67
|
+
Requires-Dist: mcp<2,>=1.27; extra == "all"
|
|
65
68
|
Provides-Extra: dev
|
|
66
69
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
67
70
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
@@ -104,6 +107,7 @@ Dynamic: license-file
|
|
|
104
107
|
- **Alert lifecycle** — consecutive anomalies, fraction-of-window rule (`anomaly_window` + `min_anomaly_share`), cooldown, recovery notifications, no-data alerts
|
|
105
108
|
- **Project-level error alerts** — catch DB outages and pipeline crashes once per run
|
|
106
109
|
- **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed)
|
|
110
|
+
- **Hybrid mode** — read metric SQL from a billed-per-query warehouse while all pipeline state lives in a cheap local database
|
|
107
111
|
- **Idempotent** — resume from interruptions, no duplicate processing
|
|
108
112
|
- **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
|
|
109
113
|
- **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + three skills) so an assistant can scaffold metrics, configure databases, and file feedback upstream
|
|
@@ -198,6 +202,18 @@ for r in results:
|
|
|
198
202
|
print(f"Anomaly at {r.timestamp}: {r.value}")
|
|
199
203
|
```
|
|
200
204
|
|
|
205
|
+
## AI & automation
|
|
206
|
+
|
|
207
|
+
- **[MCP server](docs/guides/mcp.md)** (`dtk mcp`) — a read-only [Model Context
|
|
208
|
+
Protocol](https://modelcontextprotocol.io) server so an AI assistant (Claude
|
|
209
|
+
Code, Claude Desktop, any MCP client) can query a project's metric configs,
|
|
210
|
+
loaded data, detector results, replayed alert history and autotune runs
|
|
211
|
+
directly — no write access to the pipeline.
|
|
212
|
+
- **[GitHub Action](docs/guides/github-action.md)** — a composite action that
|
|
213
|
+
runs `dtk run`/`autotune`/`clean` as a CI check or scheduled job and gates
|
|
214
|
+
on detectkit's own exit-code contract, with the `--json` run summary wired
|
|
215
|
+
to step outputs.
|
|
216
|
+
|
|
201
217
|
## Documentation
|
|
202
218
|
|
|
203
219
|
- **[dtk.pipelab.dev](https://dtk.pipelab.dev)** — full docs site, guides, reference, and a live in-browser playground
|
|
@@ -206,6 +222,8 @@ for r in results:
|
|
|
206
222
|
- [Detectors Guide](docs/guides/detectors.md) — choosing the right detector
|
|
207
223
|
- [Alerting Guide](docs/guides/alerting.md) — channels, mentions, cooldown, recovery
|
|
208
224
|
- [Reading Alerts](docs/guides/reading-alerts.md) — what a received alert means (for stakeholders)
|
|
225
|
+
- [MCP Guide](docs/guides/mcp.md) — read-only AI-assistant access with `dtk mcp`
|
|
226
|
+
- [GitHub Action Guide](docs/guides/github-action.md) — run the pipeline in CI
|
|
209
227
|
- [CLI Reference](docs/reference/cli.md) — command-line documentation
|
|
210
228
|
- [Examples](docs/examples/) — real-world monitoring scenarios
|
|
211
229
|
- [Changelog](CHANGELOG.md) — version history
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
- **Alert lifecycle** — consecutive anomalies, fraction-of-window rule (`anomaly_window` + `min_anomaly_share`), cooldown, recovery notifications, no-data alerts
|
|
26
26
|
- **Project-level error alerts** — catch DB outages and pipeline crashes once per run
|
|
27
27
|
- **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed)
|
|
28
|
+
- **Hybrid mode** — read metric SQL from a billed-per-query warehouse while all pipeline state lives in a cheap local database
|
|
28
29
|
- **Idempotent** — resume from interruptions, no duplicate processing
|
|
29
30
|
- **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
|
|
30
31
|
- **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + three skills) so an assistant can scaffold metrics, configure databases, and file feedback upstream
|
|
@@ -119,6 +120,18 @@ for r in results:
|
|
|
119
120
|
print(f"Anomaly at {r.timestamp}: {r.value}")
|
|
120
121
|
```
|
|
121
122
|
|
|
123
|
+
## AI & automation
|
|
124
|
+
|
|
125
|
+
- **[MCP server](docs/guides/mcp.md)** (`dtk mcp`) — a read-only [Model Context
|
|
126
|
+
Protocol](https://modelcontextprotocol.io) server so an AI assistant (Claude
|
|
127
|
+
Code, Claude Desktop, any MCP client) can query a project's metric configs,
|
|
128
|
+
loaded data, detector results, replayed alert history and autotune runs
|
|
129
|
+
directly — no write access to the pipeline.
|
|
130
|
+
- **[GitHub Action](docs/guides/github-action.md)** — a composite action that
|
|
131
|
+
runs `dtk run`/`autotune`/`clean` as a CI check or scheduled job and gates
|
|
132
|
+
on detectkit's own exit-code contract, with the `--json` run summary wired
|
|
133
|
+
to step outputs.
|
|
134
|
+
|
|
122
135
|
## Documentation
|
|
123
136
|
|
|
124
137
|
- **[dtk.pipelab.dev](https://dtk.pipelab.dev)** — full docs site, guides, reference, and a live in-browser playground
|
|
@@ -127,6 +140,8 @@ for r in results:
|
|
|
127
140
|
- [Detectors Guide](docs/guides/detectors.md) — choosing the right detector
|
|
128
141
|
- [Alerting Guide](docs/guides/alerting.md) — channels, mentions, cooldown, recovery
|
|
129
142
|
- [Reading Alerts](docs/guides/reading-alerts.md) — what a received alert means (for stakeholders)
|
|
143
|
+
- [MCP Guide](docs/guides/mcp.md) — read-only AI-assistant access with `dtk mcp`
|
|
144
|
+
- [GitHub Action Guide](docs/guides/github-action.md) — run the pipeline in CI
|
|
130
145
|
- [CLI Reference](docs/reference/cli.md) — command-line documentation
|
|
131
146
|
- [Examples](docs/examples/) — real-world monitoring scenarios
|
|
132
147
|
- [Changelog](CHANGELOG.md) — version history
|
|
@@ -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.62.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -18,6 +18,7 @@ Run all commands from a project directory (the one containing
|
|
|
18
18
|
| `dtk clean --select <sel>` | Prune internal data that no longer matches the config |
|
|
19
19
|
| `dtk osi import <model>` | Scaffold a native metric from an OSI semantic-model metric (see "OSI interop") |
|
|
20
20
|
| `dtk osi export` | Publish metrics into an OSI fragment (config in `custom_extensions[detectkit]`) |
|
|
21
|
+
| `dtk mcp` | Serve a **read-only** MCP server for an AI assistant (metrics, data, alerts, autotune history — no writes) |
|
|
21
22
|
| `dtk --version` | Show installed detectkit version |
|
|
22
23
|
|
|
23
24
|
## Selectors (`--select` / `-s`)
|
|
@@ -376,6 +377,40 @@ The `dtk ui` metric Builder's **From OSI** sub-tab does the import
|
|
|
376
377
|
interactively (paste a model, pick a metric/target, Compile) through the
|
|
377
378
|
same code path — see "Managing metrics from the UI" above.
|
|
378
379
|
|
|
380
|
+
## `dtk mcp`
|
|
381
|
+
|
|
382
|
+
A **read-only** [Model Context Protocol](https://modelcontextprotocol.io)
|
|
383
|
+
server, exposing a project to *another* AI assistant (an MCP client — Claude
|
|
384
|
+
Code, Claude Desktop, an IDE extension) rather than being one itself. It is a
|
|
385
|
+
separate, additive command (needs `pip install 'detectkit[mcp]'`) that
|
|
386
|
+
contains **zero write paths**: no config edits, no label writes, no pipeline
|
|
387
|
+
runs, no schema creation. Ten tools: `list_metrics`, `get_metric`,
|
|
388
|
+
`get_metric_status`, `get_project_status`, `query_datapoints`,
|
|
389
|
+
`query_detections`, `replay_alerts` (reconstructs the alert timeline the same
|
|
390
|
+
way `dtk run --report` does — never reads `_dtk_alert_states`, never
|
|
391
|
+
dispatches), `get_autotune_history`, `get_incidents`, `get_server_info`.
|
|
392
|
+
Channel connection details/secrets in `profiles.yml` are never exposed —
|
|
393
|
+
`get_metric` lists alerting channels by name only.
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
dtk mcp --project-dir /abs/path/to/project [--select <sel>] [--profile NAME]
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
- `--project-dir` — resolved before `$DETECTKIT_PROJECT_DIR` before the
|
|
400
|
+
current directory (an MCP client passes no cwd, so at least one must
|
|
401
|
+
resolve; each mechanism searches upward for `detectkit_project.yml`).
|
|
402
|
+
- `--select` — scopes the whole session: every tool that names a metric
|
|
403
|
+
refuses one outside this set (an access-control boundary, not just a
|
|
404
|
+
default).
|
|
405
|
+
|
|
406
|
+
This is a **different thing** from the generic database MCP CLAUDE.md
|
|
407
|
+
recommends you (the assistant working in this repo) optionally connect to for
|
|
408
|
+
your own read access to the project's DB — `dtk mcp` answers in detectkit's
|
|
409
|
+
own terms (replayed alert decisions, detector identity, labeled incidents),
|
|
410
|
+
not raw SQL, and it's for exposing *this* project to a *separate* assistant
|
|
411
|
+
session/client (e.g. Claude Desktop), not something you need to launch or
|
|
412
|
+
connect to yourself. `dtk mcp --help` has the full flag reference.
|
|
413
|
+
|
|
379
414
|
## Common workflows
|
|
380
415
|
|
|
381
416
|
```bash
|
|
@@ -11,6 +11,7 @@ keep them in sync. Detectors are covered in `detectors.md`, alerting in
|
|
|
11
11
|
name: api_response_time # required, unique across the project
|
|
12
12
|
description: API p95 latency # optional, shown in alerts
|
|
13
13
|
profile: prod # optional, overrides project default_profile
|
|
14
|
+
source_profile: warehouse # optional — hybrid mode: source-DB profile for THIS metric's SQL (see below)
|
|
14
15
|
enabled: true # optional, false → skipped by `dtk run`
|
|
15
16
|
tags: [critical, api] # optional, used by `--select tag:<t>`
|
|
16
17
|
ai_context: # optional — OSI-compatible grounding (descriptive only)
|
|
@@ -67,6 +68,19 @@ false_alert_budget: 0.3 # optional — `dtk tune` target false-alert rate
|
|
|
67
68
|
project-wide default; unset → project, then a built-in `0.5`. Tuning-only — it never
|
|
68
69
|
affects the load/detect/alert pipeline.
|
|
69
70
|
|
|
71
|
+
`source_profile` is **hybrid mode** for this one metric: the `profiles.yml`
|
|
72
|
+
profile whose database runs *this metric's* SQL query, while `_dtk_*` state
|
|
73
|
+
(datapoints, detections, task locks, alert state) stays in the active
|
|
74
|
+
**state** profile — the one `dtk run` is already connected to for everything
|
|
75
|
+
else. Overrides the project-wide `source_profile` (`project.md`); resolves
|
|
76
|
+
**metric → project → unset** (unset on both = today's behavior — one profile
|
|
77
|
+
for everything). Not the same field as `profile` above (`profile` is dead —
|
|
78
|
+
not applied at runtime by `dtk run`; `source_profile` is live). Only the
|
|
79
|
+
metric's **load** step ever connects to it — detect/alert and every other
|
|
80
|
+
command don't — though every `dtk run` fail-fast validates the resolved name
|
|
81
|
+
up front regardless of `--steps` (a typo exits 1 before any pipeline work).
|
|
82
|
+
See the [Hybrid Mode guide](https://dtk.pipelab.dev/guides/hybrid-mode/).
|
|
83
|
+
|
|
70
84
|
`ai_context` is **OSI-compatible grounding** you can add to any metric with **no
|
|
71
85
|
OSI model needed** — the metric's business meaning (`instructions`), alternative
|
|
72
86
|
names (`synonyms`) and example values (`examples`), mirroring the
|
|
@@ -33,6 +33,7 @@ timeouts: # per-step, seconds
|
|
|
33
33
|
alert_help_url: null # optional, see below — "How to read this alert" link
|
|
34
34
|
false_alert_budget: null # optional — `dtk tune` target FDR (0,1]; per-metric override wins
|
|
35
35
|
loading_delay: null # optional — default data-maturity delay; per-metric override wins
|
|
36
|
+
source_profile: null # optional — hybrid mode: default source-DB profile; per-metric override wins
|
|
36
37
|
|
|
37
38
|
error_alerting: # optional, see below
|
|
38
39
|
enabled: false
|
|
@@ -50,6 +51,26 @@ overrides it (`0` opts that metric out); see `metrics.md` for the full
|
|
|
50
51
|
load/no-data behavior and the detection-latency trade-off. Only set this
|
|
51
52
|
project-wide when your metrics genuinely share the same upstream schedule.
|
|
52
53
|
|
|
54
|
+
`source_profile` is **hybrid mode**: a project-wide default `profiles.yml`
|
|
55
|
+
profile whose database runs metric SQL, while *every* `_dtk_*` table
|
|
56
|
+
(datapoints, detections, task locks, alert state) stays in the active
|
|
57
|
+
**state** profile — the one `dtk run` is already connected to for everything
|
|
58
|
+
else. A per-metric `source_profile` overrides it (`metrics.md`); resolves
|
|
59
|
+
**metric → project → unset**, same precedence as `loading_delay`. Unset on
|
|
60
|
+
both = today's behavior (one profile does everything). Only the LOAD step's
|
|
61
|
+
metric-SQL query is affected — detect/alert and every other command
|
|
62
|
+
(`dtk autotune`/`tune`/`ui`/`clean`/`unlock`) only ever touch the state
|
|
63
|
+
profile. A source-side failure raises `SourceDatabaseError` (message leads
|
|
64
|
+
with `source database (profile '<name>'): ...`), distinct from a plain
|
|
65
|
+
exception for a state-side failure — useful for telling a warehouse outage
|
|
66
|
+
apart from a state-DB outage in `error_alerting`'s `{error_type}` /
|
|
67
|
+
`{error_message}`. `dtk run` validates every resolved `source_profile` name
|
|
68
|
+
against `profiles.yml` before opening any connection (unknown name → exit
|
|
69
|
+
`1`, no partial run). Full config example + operational caveats (connecting a
|
|
70
|
+
profile always runs `CREATE DATABASE`/`SCHEMA IF NOT EXISTS` for its own
|
|
71
|
+
`internal_*` location too, even a source-only one) in the [Hybrid Mode
|
|
72
|
+
guide](https://dtk.pipelab.dev/guides/hybrid-mode/).
|
|
73
|
+
|
|
53
74
|
### `alert_help_url` — "How to read this alert" link
|
|
54
75
|
|
|
55
76
|
Every default-rendered alert on every channel carries a `How to read this alert`
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Implementation of 'dtk mcp' — the read-only Model Context Protocol server.
|
|
2
|
+
|
|
3
|
+
Isolated from the load/detect/alert pipeline: this module imports only
|
|
4
|
+
``click`` at load time. Everything else — :mod:`detectkit.mcp.server` and the
|
|
5
|
+
optional ``mcp`` SDK it lazily guards — is imported inside :func:`run_mcp`'s
|
|
6
|
+
body, so `import detectkit.cli.commands.mcp` never pulls in the SDK either.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_mcp(*, project_dir: str | None, select: str, profile: str | None) -> None:
|
|
15
|
+
"""Resolve the project and serve the read-only MCP server over stdio.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
project_dir: Explicit project directory (searched upward for
|
|
19
|
+
``detectkit_project.yml``). Falls back to ``$DETECTKIT_PROJECT_DIR``,
|
|
20
|
+
then to searching upward from the current directory.
|
|
21
|
+
select: Selector scoping which metrics the server exposes.
|
|
22
|
+
profile: Profile name to use (defaults to the project's
|
|
23
|
+
``default_profile``).
|
|
24
|
+
"""
|
|
25
|
+
from detectkit.mcp.errors import McpError
|
|
26
|
+
from detectkit.mcp.server import run_server
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
run_server(project_dir=project_dir, selector=select, profile=profile)
|
|
30
|
+
except McpError as exc:
|
|
31
|
+
raise click.ClickException(str(exc)) from exc
|
|
@@ -13,7 +13,7 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
import click
|
|
15
15
|
|
|
16
|
-
from detectkit.config.metric_config import MetricConfig
|
|
16
|
+
from detectkit.config.metric_config import MetricConfig, resolve_source_profile
|
|
17
17
|
from detectkit.config.profile import ProfilesConfig
|
|
18
18
|
from detectkit.config.project_config import ProjectConfig
|
|
19
19
|
from detectkit.config.validator import (
|
|
@@ -326,6 +326,21 @@ def _run_impl(
|
|
|
326
326
|
summary["error"] = f"Error loading profiles.yml: {e}"
|
|
327
327
|
return 1
|
|
328
328
|
|
|
329
|
+
# Fail-fast: every selected metric's RESOLVED source_profile (hybrid
|
|
330
|
+
# mode) must name a real profile. This is a cheap name check — no
|
|
331
|
+
# connections opened — so a typo'd source_profile fails the whole run
|
|
332
|
+
# up front instead of surfacing deep inside whichever metric's LOAD step
|
|
333
|
+
# happens to hit it first.
|
|
334
|
+
source_profile_error = _validate_source_profiles(metrics, project_config, profiles_config)
|
|
335
|
+
if source_profile_error:
|
|
336
|
+
# A config typo, not an outage: exit 1 without paging error_alerting,
|
|
337
|
+
# consistent with the sibling config-error paths (bad selector,
|
|
338
|
+
# unparseable profiles.yml). The error alert channel is reserved for
|
|
339
|
+
# DB-down / DDL / runtime failures.
|
|
340
|
+
click.echo(click.style(f"Error: {source_profile_error}", fg="red", bold=True))
|
|
341
|
+
summary["error"] = f"Error: {source_profile_error}"
|
|
342
|
+
return 1
|
|
343
|
+
|
|
329
344
|
# Create database manager
|
|
330
345
|
try:
|
|
331
346
|
db_manager = profiles_config.create_manager(profile)
|
|
@@ -372,84 +387,136 @@ def _run_impl(
|
|
|
372
387
|
summary["error"] = f"Error initializing internal tables: {e}"
|
|
373
388
|
return 1
|
|
374
389
|
|
|
390
|
+
# The active STATE profile's actual name (mirrors ProfilesConfig.get_profile's
|
|
391
|
+
# own None -> default_profile resolution) — lets the task manager recognize
|
|
392
|
+
# a metric's source_profile that happens to name the state profile itself,
|
|
393
|
+
# and reuse db_manager instead of opening a duplicate connection.
|
|
394
|
+
state_profile_name: str | None
|
|
395
|
+
if profile is not None:
|
|
396
|
+
state_profile_name = profile
|
|
397
|
+
else:
|
|
398
|
+
state_profile_name = getattr(profiles_config, "default_profile", None)
|
|
399
|
+
|
|
375
400
|
# Create task manager
|
|
376
401
|
task_manager = TaskManager(
|
|
377
402
|
internal_manager=internal_manager,
|
|
378
403
|
db_manager=db_manager,
|
|
379
404
|
profiles_config=profiles_config,
|
|
380
405
|
project_config=project_config,
|
|
406
|
+
state_profile_name=state_profile_name,
|
|
381
407
|
)
|
|
382
408
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
# Project-level error alert was dispatched — stop processing the
|
|
409
|
-
# rest of the metrics. The DB / source is presumed unreachable;
|
|
410
|
-
# subsequent metrics would all fail with the same error.
|
|
411
|
-
if result.get("abort_run"):
|
|
412
|
-
click.echo(
|
|
413
|
-
click.style(
|
|
414
|
-
"✗ Aborting run after project error alert. " "Remaining metrics skipped.",
|
|
415
|
-
fg="red",
|
|
416
|
-
bold=True,
|
|
417
|
-
)
|
|
409
|
+
try:
|
|
410
|
+
# Process each metric
|
|
411
|
+
for index, (metric_path, config) in enumerate(metrics):
|
|
412
|
+
result = process_metric(
|
|
413
|
+
metric_path=metric_path,
|
|
414
|
+
config=config,
|
|
415
|
+
project_root=project_root,
|
|
416
|
+
task_manager=task_manager,
|
|
417
|
+
steps=step_list,
|
|
418
|
+
from_date=from_dt,
|
|
419
|
+
to_date=to_dt,
|
|
420
|
+
full_refresh=full_refresh,
|
|
421
|
+
force=force,
|
|
422
|
+
)
|
|
423
|
+
summary["metrics"].append(
|
|
424
|
+
{
|
|
425
|
+
"name": config.name,
|
|
426
|
+
"status": _status_str(result["status"]),
|
|
427
|
+
"steps_completed": [s.value for s in result["steps_completed"]],
|
|
428
|
+
"datapoints_loaded": result["datapoints_loaded"],
|
|
429
|
+
"anomalies_detected": result["anomalies_detected"],
|
|
430
|
+
"alerts_sent": result["alerts_sent"],
|
|
431
|
+
"error": result["error"],
|
|
432
|
+
}
|
|
418
433
|
)
|
|
419
|
-
summary["aborted"] = True
|
|
420
|
-
for _, skipped_config in metrics[index + 1 :]:
|
|
421
|
-
summary["metrics"].append(
|
|
422
|
-
{
|
|
423
|
-
"name": skipped_config.name,
|
|
424
|
-
"status": "skipped",
|
|
425
|
-
"steps_completed": [],
|
|
426
|
-
"datapoints_loaded": 0,
|
|
427
|
-
"anomalies_detected": 0,
|
|
428
|
-
"alerts_sent": 0,
|
|
429
|
-
"error": None,
|
|
430
|
-
}
|
|
431
|
-
)
|
|
432
|
-
break
|
|
433
434
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
project_loading_delay=getattr(project_config, "loading_delay", None),
|
|
445
|
-
from_dt=from_dt,
|
|
446
|
-
to_dt=to_dt,
|
|
435
|
+
# Project-level error alert was dispatched — stop processing the
|
|
436
|
+
# rest of the metrics. The DB / source is presumed unreachable;
|
|
437
|
+
# subsequent metrics would all fail with the same error.
|
|
438
|
+
if result.get("abort_run"):
|
|
439
|
+
click.echo(
|
|
440
|
+
click.style(
|
|
441
|
+
"✗ Aborting run after project error alert. " "Remaining metrics skipped.",
|
|
442
|
+
fg="red",
|
|
443
|
+
bold=True,
|
|
444
|
+
)
|
|
447
445
|
)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
446
|
+
summary["aborted"] = True
|
|
447
|
+
for _, skipped_config in metrics[index + 1 :]:
|
|
448
|
+
summary["metrics"].append(
|
|
449
|
+
{
|
|
450
|
+
"name": skipped_config.name,
|
|
451
|
+
"status": "skipped",
|
|
452
|
+
"steps_completed": [],
|
|
453
|
+
"datapoints_loaded": 0,
|
|
454
|
+
"anomalies_detected": 0,
|
|
455
|
+
"alerts_sent": 0,
|
|
456
|
+
"error": None,
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
break
|
|
460
|
+
|
|
461
|
+
# Optional: emit a self-contained HTML report from the freshly-persisted
|
|
462
|
+
# internal tables (values + bands + anomalies + replayed alerts).
|
|
463
|
+
if report_path is not None:
|
|
464
|
+
try:
|
|
465
|
+
emit_metric_report(
|
|
466
|
+
config=config,
|
|
467
|
+
project_root=project_root,
|
|
468
|
+
internal_manager=internal_manager,
|
|
469
|
+
report_path=report_path,
|
|
470
|
+
project_name=getattr(project_config, "name", None),
|
|
471
|
+
project_loading_delay=getattr(project_config, "loading_delay", None),
|
|
472
|
+
from_dt=from_dt,
|
|
473
|
+
to_dt=to_dt,
|
|
474
|
+
)
|
|
475
|
+
except Exception as report_error: # never fail the run on a report
|
|
476
|
+
click.echo(click.style(f" │ Report skipped: {report_error}", fg="yellow"))
|
|
477
|
+
|
|
478
|
+
failed = any(m["status"] == "failed" for m in summary["metrics"])
|
|
479
|
+
return 1 if (failed or summary["aborted"]) else 0
|
|
480
|
+
finally:
|
|
481
|
+
# Close every pooled hybrid-mode SOURCE manager (never db_manager
|
|
482
|
+
# itself — this function owns that connection's lifecycle, and it
|
|
483
|
+
# was never explicitly closed here either, before or after hybrid
|
|
484
|
+
# mode existed). Duck-typed lookup: a test double standing in for
|
|
485
|
+
# TaskManager need not implement it.
|
|
486
|
+
close_sources = getattr(task_manager, "close_sources", None)
|
|
487
|
+
if close_sources is not None:
|
|
488
|
+
close_sources()
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _validate_source_profiles(
|
|
492
|
+
metrics: list[tuple[Path, MetricConfig]],
|
|
493
|
+
project_config: ProjectConfig,
|
|
494
|
+
profiles_config: ProfilesConfig,
|
|
495
|
+
) -> str | None:
|
|
496
|
+
"""Cheap, connection-free check that every metric's RESOLVED
|
|
497
|
+
``source_profile`` (hybrid mode) names a real profile.
|
|
498
|
+
|
|
499
|
+
Returns an error message naming every offending metric, or ``None`` when
|
|
500
|
+
every resolved name is either unset (no hybrid override) or a known
|
|
501
|
+
profile. Runs before any database manager is built.
|
|
502
|
+
"""
|
|
503
|
+
project_source_profile = getattr(project_config, "source_profile", None)
|
|
504
|
+
known_profiles = getattr(profiles_config, "profiles", {}) or {}
|
|
505
|
+
unknown: dict[str, str] = {}
|
|
506
|
+
for _, config in metrics:
|
|
507
|
+
name = resolve_source_profile(
|
|
508
|
+
getattr(config, "source_profile", None), project_source_profile
|
|
509
|
+
)
|
|
510
|
+
if name is not None and name not in known_profiles:
|
|
511
|
+
unknown[config.name] = name
|
|
512
|
+
if not unknown:
|
|
513
|
+
return None
|
|
514
|
+
details = ", ".join(f"{m} -> '{p}'" for m, p in sorted(unknown.items()))
|
|
515
|
+
available = ", ".join(sorted(known_profiles.keys()))
|
|
516
|
+
return (
|
|
517
|
+
f"Unknown source_profile referenced by metric(s): {details}. "
|
|
518
|
+
f"Available profiles: {available}"
|
|
519
|
+
)
|
|
453
520
|
|
|
454
521
|
|
|
455
522
|
def _resolve_report_path(report_path: str, project_root: Path, metric_name: str) -> Path:
|
|
@@ -592,5 +592,61 @@ from detectkit.cli.commands.osi import osi as _osi_group # noqa: E402
|
|
|
592
592
|
cli.add_command(_osi_group)
|
|
593
593
|
|
|
594
594
|
|
|
595
|
+
# MCP (Model Context Protocol) server — a self-contained, READ-ONLY command
|
|
596
|
+
# (`dtk mcp`), unlike `dtk osi` a single command rather than a group. Isolated
|
|
597
|
+
# from the load/detect/alert pipeline: nothing in load/detect/alert imports
|
|
598
|
+
# detectkit.mcp, and this module only imports click at load time — the mcp SDK
|
|
599
|
+
# (the optional `[mcp]` extra) and detectkit.mcp.server are imported lazily,
|
|
600
|
+
# inside the command body.
|
|
601
|
+
@cli.command()
|
|
602
|
+
@click.option(
|
|
603
|
+
"--project-dir",
|
|
604
|
+
default=None,
|
|
605
|
+
help=(
|
|
606
|
+
"Project directory (searched upward for detectkit_project.yml). "
|
|
607
|
+
"Default: $DETECTKIT_PROJECT_DIR, else search upward from the current "
|
|
608
|
+
"directory. An MCP client passes no cwd, so at least one of these must "
|
|
609
|
+
"resolve."
|
|
610
|
+
),
|
|
611
|
+
)
|
|
612
|
+
@click.option(
|
|
613
|
+
"--select",
|
|
614
|
+
"-s",
|
|
615
|
+
default="*",
|
|
616
|
+
show_default=True,
|
|
617
|
+
help="Selector scoping which metrics the server exposes (metric name, path, or tag)",
|
|
618
|
+
)
|
|
619
|
+
@click.option(
|
|
620
|
+
"--profile",
|
|
621
|
+
help="Profile to use (default: from project config)",
|
|
622
|
+
)
|
|
623
|
+
def mcp(project_dir: str | None, select: str, profile: str | None) -> None:
|
|
624
|
+
"""
|
|
625
|
+
Serve a read-only Model Context Protocol (MCP) server over stdio.
|
|
626
|
+
|
|
627
|
+
Exposes a detectkit project to an MCP client (an IDE assistant, a desktop
|
|
628
|
+
app, a cloud agent): metric configs, loaded datapoints, detector results,
|
|
629
|
+
replayed alert history, autotune runs, and labeled incidents. Nothing in
|
|
630
|
+
this server writes to the database or the filesystem — no config edits,
|
|
631
|
+
no label writes, no pipeline runs, no schema creation. It never calls
|
|
632
|
+
`ensure_tables()`; a project with no data yet answers tool calls with a
|
|
633
|
+
clear "run `dtk run` first" error instead of creating tables.
|
|
634
|
+
|
|
635
|
+
`--select` scopes the whole session: every tool that names a metric
|
|
636
|
+
refuses one outside this set, and list/status tools intersect their own
|
|
637
|
+
selector with it. Needs the `[mcp]` extra: pip install 'detectkit[mcp]'.
|
|
638
|
+
|
|
639
|
+
Examples:
|
|
640
|
+
# Expose every metric in the project the current directory is inside
|
|
641
|
+
dtk mcp
|
|
642
|
+
|
|
643
|
+
# Scope to one tag, from a project the client doesn't cd into
|
|
644
|
+
dtk mcp --project-dir /opt/monitoring --select "tag:critical"
|
|
645
|
+
"""
|
|
646
|
+
from detectkit.cli.commands.mcp import run_mcp
|
|
647
|
+
|
|
648
|
+
run_mcp(project_dir=project_dir, select=select, profile=profile)
|
|
649
|
+
|
|
650
|
+
|
|
595
651
|
if __name__ == "__main__":
|
|
596
652
|
cli()
|
|
@@ -627,6 +627,8 @@ class MetricConfig(BaseModel):
|
|
|
627
627
|
description: Optional metric description (supports multi-line text)
|
|
628
628
|
tags: Optional list of tags for metric selection (e.g., ["critical", "api"])
|
|
629
629
|
profile: Profile name to use (overrides default_profile from project config)
|
|
630
|
+
source_profile: Hybrid mode — profile whose database runs this metric's
|
|
631
|
+
SQL, while _dtk_* state stays in the active (state) profile
|
|
630
632
|
query: Inline SQL query (mutually exclusive with query_file)
|
|
631
633
|
query_file: Path to SQL file (mutually exclusive with query)
|
|
632
634
|
query_columns: Column name mapping for query results
|
|
@@ -684,6 +686,22 @@ class MetricConfig(BaseModel):
|
|
|
684
686
|
profile: str | None = Field(
|
|
685
687
|
default=None, description="Profile name to use (overrides default_profile)"
|
|
686
688
|
)
|
|
689
|
+
# Hybrid mode: run this metric's SQL against a different database than the
|
|
690
|
+
# one holding _dtk_* state. Unlike `profile` above (which is dead — only
|
|
691
|
+
# round-tripped by autotune's config emitter), this is live: the load step
|
|
692
|
+
# resolves it to a pooled `ProfilesConfig.create_manager(...)` and executes
|
|
693
|
+
# the metric's query through it, while every other step (detect/alert) and
|
|
694
|
+
# save_datapoints itself keep using the active state profile. Falls back to
|
|
695
|
+
# ProjectConfig.source_profile, then None (= use the state profile).
|
|
696
|
+
source_profile: str | None = Field(
|
|
697
|
+
default=None,
|
|
698
|
+
description=(
|
|
699
|
+
"profiles.yml profile whose database runs this metric's SQL; "
|
|
700
|
+
"_dtk_* state stays in the active (state) profile. Falls back to "
|
|
701
|
+
"the project-level source_profile, then to the state profile "
|
|
702
|
+
"itself."
|
|
703
|
+
),
|
|
704
|
+
)
|
|
687
705
|
query: str | None = Field(default=None, description="Inline SQL query")
|
|
688
706
|
query_file: Path | None = Field(default=None, description="Path to SQL file")
|
|
689
707
|
query_columns: QueryColumnsConfig | None = Field(
|
|
@@ -1054,6 +1072,24 @@ def resolve_loading_delay_seconds(
|
|
|
1054
1072
|
return 0
|
|
1055
1073
|
|
|
1056
1074
|
|
|
1075
|
+
def resolve_source_profile(
|
|
1076
|
+
metric_source_profile: str | None, project_source_profile: str | None
|
|
1077
|
+
) -> str | None:
|
|
1078
|
+
"""Resolve the profiles.yml profile that should run a metric's SQL:
|
|
1079
|
+
metric -> project -> ``None``.
|
|
1080
|
+
|
|
1081
|
+
``None`` means "run the SQL through the same connection detectkit is
|
|
1082
|
+
already using for ``_dtk_*`` state" — i.e. hybrid mode is off for this
|
|
1083
|
+
metric and the meaning of ``dtk run --profile`` / ``default_profile`` is
|
|
1084
|
+
unchanged. The first configured value wins, mirroring
|
|
1085
|
+
:func:`resolve_loading_delay_seconds`'s metric -> project precedence.
|
|
1086
|
+
"""
|
|
1087
|
+
for value in (metric_source_profile, project_source_profile):
|
|
1088
|
+
if value is not None:
|
|
1089
|
+
return value
|
|
1090
|
+
return None
|
|
1091
|
+
|
|
1092
|
+
|
|
1057
1093
|
def resolve_grid_phase_seconds(loading_start_time: str | None, interval_seconds: int) -> int:
|
|
1058
1094
|
"""Phase of the metric's interval grid on the epoch clock, in ``[0, interval)``.
|
|
1059
1095
|
|