detectkit 0.45.1__tar.gz → 0.47.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.45.1/detectkit.egg-info → detectkit-0.47.0}/PKG-INFO +5 -1
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/__init__.py +1 -1
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/channels/base.py +17 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/channels/webhook.py +145 -129
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_base.py +5 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_decision.py +2 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_recovery.py +1 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/CLAUDE.section.md +1 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/alerting.md +21 -20
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/cli.md +40 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/metrics.md +16 -0
- detectkit-0.47.0/detectkit/cli/commands/osi.py +245 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/main.py +8 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/config/metric_config.py +76 -1
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_alert_step.py +1 -0
- detectkit-0.47.0/detectkit/semantic/__init__.py +41 -0
- detectkit-0.47.0/detectkit/semantic/errors.py +31 -0
- detectkit-0.47.0/detectkit/semantic/exporter.py +102 -0
- detectkit-0.47.0/detectkit/semantic/importer.py +281 -0
- detectkit-0.47.0/detectkit/semantic/osi_model.py +221 -0
- detectkit-0.47.0/detectkit/semantic/query_gen.py +229 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/tuning/payload.py +20 -0
- {detectkit-0.45.1 → detectkit-0.47.0/detectkit.egg-info}/PKG-INFO +5 -1
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit.egg-info/SOURCES.txt +7 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit.egg-info/requires.txt +5 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/pyproject.toml +7 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/LICENSE +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/MANIFEST.in +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/README.md +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/core/models.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/tuning/assets/tune.js +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/requirements.txt +0 -0
- {detectkit-0.45.1 → detectkit-0.47.0}/setup.cfg +0 -0
- {detectkit-0.45.1 → detectkit-0.47.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.47.0
|
|
4
4
|
Summary: Metric monitoring with automatic anomaly detection
|
|
5
5
|
Author: detectkit team
|
|
6
6
|
License: MIT
|
|
@@ -47,12 +47,15 @@ Requires-Dist: timesfm>=0.1.0; extra == "timesfm"
|
|
|
47
47
|
Provides-Extra: advanced-detectors
|
|
48
48
|
Requires-Dist: prophet>=1.1.0; extra == "advanced-detectors"
|
|
49
49
|
Requires-Dist: timesfm>=0.1.0; extra == "advanced-detectors"
|
|
50
|
+
Provides-Extra: osi
|
|
51
|
+
Requires-Dist: sqlglot>=20.0; extra == "osi"
|
|
50
52
|
Provides-Extra: all
|
|
51
53
|
Requires-Dist: clickhouse-driver>=0.2.0; extra == "all"
|
|
52
54
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
53
55
|
Requires-Dist: pymysql>=1.0.0; extra == "all"
|
|
54
56
|
Requires-Dist: prophet>=1.1.0; extra == "all"
|
|
55
57
|
Requires-Dist: timesfm>=0.1.0; extra == "all"
|
|
58
|
+
Requires-Dist: sqlglot>=20.0; extra == "all"
|
|
56
59
|
Provides-Extra: dev
|
|
57
60
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
58
61
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
@@ -61,6 +64,7 @@ Requires-Dist: requests-mock>=1.12; extra == "dev"
|
|
|
61
64
|
Requires-Dist: black>=23.0; extra == "dev"
|
|
62
65
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
63
66
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
67
|
+
Requires-Dist: sqlglot>=20.0; extra == "dev"
|
|
64
68
|
Provides-Extra: integration
|
|
65
69
|
Requires-Dist: testcontainers[clickhouse,mysql,postgres]>=4.0; extra == "integration"
|
|
66
70
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "integration"
|
|
@@ -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.47.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -72,6 +72,13 @@ class AlertData:
|
|
|
72
72
|
error_message: str | None = None
|
|
73
73
|
description: str | None = None
|
|
74
74
|
mentions: list[str] = field(default_factory=list)
|
|
75
|
+
# OSI-compatible AI grounding: the metric's alternative names. Exposed to
|
|
76
|
+
# message templates as the OPT-IN ``{synonyms}`` / ``{synonyms_line}``
|
|
77
|
+
# variables (via :meth:`build_context`) so a custom template can surface an
|
|
78
|
+
# "Also known as: …" line — but the DEFAULT rendering deliberately does NOT
|
|
79
|
+
# show it, so existing alerts are byte-identical. Stamped by the orchestrator
|
|
80
|
+
# from ``MetricConfig.ai_context.synonyms``; defaults to empty.
|
|
81
|
+
ai_synonyms: list[str] = field(default_factory=list)
|
|
75
82
|
project_name: str | None = None
|
|
76
83
|
# Optional actionable links surfaced in the message. ``dashboard_url`` is the
|
|
77
84
|
# headline link (rendered natively as a clickable title/link on
|
|
@@ -416,6 +423,14 @@ class BaseAlertChannel(ABC):
|
|
|
416
423
|
# Format description line (empty string if no description)
|
|
417
424
|
description_line = f"{alert_data.description}\n" if alert_data.description else ""
|
|
418
425
|
|
|
426
|
+
# OSI ai_context synonyms — the metric's alternative names. Provided as
|
|
427
|
+
# OPT-IN template variables only (``{synonyms}`` / ``{synonyms_line}``);
|
|
428
|
+
# the default templates do NOT reference them, so a metric without
|
|
429
|
+
# ai_context — and every existing alert — renders unchanged. A custom
|
|
430
|
+
# template can drop ``{synonyms_line}`` in to show "Also known as: …".
|
|
431
|
+
synonyms = ", ".join(alert_data.ai_synonyms) if alert_data.ai_synonyms else ""
|
|
432
|
+
synonyms_line = f"Also known as: {synonyms}\n" if synonyms else ""
|
|
433
|
+
|
|
419
434
|
# Format mentions
|
|
420
435
|
mentions_str = self.format_mentions(alert_data.mentions)
|
|
421
436
|
mentions_line = f"\n{mentions_str}" if mentions_str else ""
|
|
@@ -482,6 +497,8 @@ class BaseAlertChannel(ABC):
|
|
|
482
497
|
"error_message": alert_data.error_message or "",
|
|
483
498
|
"description": alert_data.description or "",
|
|
484
499
|
"description_line": description_line,
|
|
500
|
+
"synonyms": synonyms,
|
|
501
|
+
"synonyms_line": synonyms_line,
|
|
485
502
|
"dashboard_url": dashboard_url,
|
|
486
503
|
"dashboard_line": dashboard_line,
|
|
487
504
|
"help_url": help_url,
|
|
@@ -25,22 +25,23 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
25
25
|
- Slack incoming webhooks
|
|
26
26
|
- Custom webhook endpoints
|
|
27
27
|
|
|
28
|
-
Rendering: the default (no custom ``template``)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
Rendering: the default (no custom ``template``) payload is a **single
|
|
29
|
+
status-colored Slack/Mattermost attachment** whose whole body rides in one
|
|
30
|
+
markdown ``text`` block, ordered most-important-first — the lead + **Rule**
|
|
31
|
+
chip, then Value / Expected, the action **Links**, and finally the verbose
|
|
32
|
+
evidence tail (Quorum / Severity / the anomalous span / Detectors /
|
|
33
|
+
Parameters). Both platforms natively collapse a long attachment ``text``
|
|
34
|
+
behind a **"Show more"** toggle (Slack above 700 characters / 5 line breaks;
|
|
35
|
+
Mattermost wraps only the ``text`` in its ``maxHeight`` 200px fold — the
|
|
36
|
+
``title``, the color bar and the ``footer`` render *outside* that fold), so a
|
|
37
|
+
long anomaly folds its tail exactly like a reference AlertManager alert while
|
|
38
|
+
the one colored bar, the clickable title and the **branded footer (with the
|
|
39
|
+
logo)** stay in view even when the body is collapsed. No-data / error alerts
|
|
40
|
+
stay short, single un-folded cards; a long anomaly — or a full recovery
|
|
41
|
+
timeline (onset → fired → recovered) — folds its tail. A custom ``template``
|
|
42
|
+
renders the same shape — one colored, branded, text-only attachment.
|
|
43
|
+
Branding (``footer`` + ``footer_icon``, the brand logo) rides on that single
|
|
44
|
+
attachment and, being outside the text fold, is always visible.
|
|
44
45
|
|
|
45
46
|
Mentions ride in the **top-level** ``text`` (mentions inside attachments do
|
|
46
47
|
not reliably notify on Slack). A ``dashboard_url`` makes the attachment
|
|
@@ -55,10 +56,16 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
55
56
|
"text": "<!here> ...", # mentions (top level)
|
|
56
57
|
"channel": "#channel", # optional (Slack)
|
|
57
58
|
"attachments": [
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
# one colored card; the long body folds behind "Show more",
|
|
60
|
+
# the footer (brand + logo) stays visible below the fold.
|
|
61
|
+
{
|
|
62
|
+
"color": ...,
|
|
63
|
+
"title": ...,
|
|
64
|
+
"text": "<lead + Rule + value/expected + links + tail>",
|
|
65
|
+
"footer": "detectkit · <project>",
|
|
66
|
+
"footer_icon": "https://.../bot-icon.png",
|
|
67
|
+
"mrkdwn_in": ["text"],
|
|
68
|
+
},
|
|
62
69
|
]
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -134,8 +141,9 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
134
141
|
Args:
|
|
135
142
|
alert_data: Alert data to send
|
|
136
143
|
template: Optional custom message template. When given, the
|
|
137
|
-
attachment carries the formatted template as
|
|
138
|
-
|
|
144
|
+
attachment carries the formatted template verbatim as the body
|
|
145
|
+
text (in place of the structured lead/Value/tail sections);
|
|
146
|
+
otherwise the rich default is built.
|
|
139
147
|
|
|
140
148
|
Returns:
|
|
141
149
|
True if sent successfully, False otherwise
|
|
@@ -199,16 +207,18 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
199
207
|
else:
|
|
200
208
|
attachments = self._build_rich_attachments(alert_data, ctx, color, title)
|
|
201
209
|
|
|
202
|
-
# Dashboard link → clickable title on the
|
|
210
|
+
# Dashboard link → clickable title on the attachment.
|
|
203
211
|
if alert_data.dashboard_url:
|
|
204
212
|
attachments[0]["title_link"] = alert_data.dashboard_url
|
|
205
213
|
|
|
206
|
-
# Brand the
|
|
207
|
-
# top-level username/icon are locked to the app install)
|
|
208
|
-
#
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
214
|
+
# Brand the (single) attachment's footer (reliable on Slack even when
|
|
215
|
+
# top-level username/icon are locked to the app install). Mattermost and
|
|
216
|
+
# Slack render an attachment's footer *outside* the "Show more" text
|
|
217
|
+
# fold, so the brand line — and its logo (``footer_icon``) — stays
|
|
218
|
+
# visible at the bottom of the message even when the body is collapsed.
|
|
219
|
+
# Pair the brand name with the project name when set
|
|
220
|
+
# ("detectkit · my_project") so two projects posting to the same channel
|
|
221
|
+
# stay distinguishable even past the title.
|
|
212
222
|
footer = self.username or BRAND_USERNAME
|
|
213
223
|
if alert_data.project_name:
|
|
214
224
|
footer = f"{footer} · {alert_data.project_name}"
|
|
@@ -252,114 +262,131 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
252
262
|
color: str,
|
|
253
263
|
title: str,
|
|
254
264
|
) -> list[dict[str, Any]]:
|
|
255
|
-
"""Build the default attachment
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
265
|
+
"""Build the default **single** attachment for *alert_data*.
|
|
266
|
+
|
|
267
|
+
Everything rides in one status-colored attachment whose body is a single
|
|
268
|
+
markdown ``text`` block, ordered most-important-first: the lead + Rule
|
|
269
|
+
chip, then value / expected, the action links, and finally the verbose
|
|
270
|
+
evidence tail (quorum, severity, the anomalous span, detectors,
|
|
271
|
+
parameters). Mattermost wraps only an attachment's ``text`` in its
|
|
272
|
+
"Show more" fold (``maxHeight`` 200px) and renders the ``title``, color
|
|
273
|
+
bar and ``footer`` outside it; Slack folds a long ``text`` too. So a long
|
|
274
|
+
anomaly collapses its tail behind "Show more" — exactly the one-block,
|
|
275
|
+
one-color, foldable layout of a reference AlertManager alert — while the
|
|
276
|
+
colored bar, the clickable title and the branded footer/logo (attached in
|
|
277
|
+
:meth:`build_payload`) stay in view even when collapsed. No-data / error
|
|
278
|
+
stay short, single un-folded cards; a long anomaly (or a full recovery
|
|
279
|
+
timeline) folds its tail.
|
|
266
280
|
"""
|
|
267
281
|
kind = self.status_kind(alert_data)
|
|
268
|
-
base_fields: list[dict[str, Any]] = []
|
|
269
|
-
detail_lines: list[str] = []
|
|
270
|
-
|
|
271
|
-
def short(name: str, value: str) -> None:
|
|
272
|
-
"""Add a 2-col field to the always-visible base card."""
|
|
273
|
-
if value:
|
|
274
|
-
base_fields.append({"title": name, "value": value, "short": True})
|
|
275
|
-
|
|
276
|
-
def full(name: str, value: str) -> None:
|
|
277
|
-
"""Add a full-width field to the always-visible base card."""
|
|
278
|
-
if value:
|
|
279
|
-
base_fields.append({"title": name, "value": value, "short": False})
|
|
280
|
-
|
|
281
|
-
def detail(name: str, value: str) -> None:
|
|
282
|
-
"""Add one line to the foldable detail card: bold label + value."""
|
|
283
|
-
if value:
|
|
284
|
-
detail_lines.append(f"{self._bold(name)} {value}")
|
|
285
282
|
|
|
286
283
|
def code(s: str) -> str:
|
|
287
284
|
return f"`{s}`" if s else ""
|
|
288
285
|
|
|
286
|
+
def line(name: str, value: str) -> str:
|
|
287
|
+
"""One ``bold label + value`` body line (empty value → dropped)."""
|
|
288
|
+
return f"{self._bold(name)} {value}" if value else ""
|
|
289
|
+
|
|
289
290
|
# The configured firing rule, set apart as a bold "Rule" label + an
|
|
290
291
|
# inline-code chip so it reads as "this is the config that fired" at a
|
|
291
292
|
# glance. Backticks render identically on Slack and Mattermost; the bold
|
|
292
|
-
# label is platform-aware (see ``_bold``).
|
|
293
|
-
# base lead.
|
|
293
|
+
# label is platform-aware (see ``_bold``). Leads the body, above the fold.
|
|
294
294
|
rule_chip = f"{self._bold('Rule')} " + code(
|
|
295
295
|
f"min_detectors={ctx['min_detectors']} · "
|
|
296
296
|
f"direction={ctx['direction_policy']} · "
|
|
297
297
|
f"consecutive={ctx['consecutive_required']}"
|
|
298
298
|
)
|
|
299
299
|
|
|
300
|
+
# Links — compact clickable labels (never raw URL strings: a Grafana URL
|
|
301
|
+
# can be a paragraph long once it carries variables), rendered in the
|
|
302
|
+
# platform's link syntax and joined by " · ". Kept high in the body so
|
|
303
|
+
# they stay above the fold and actionable at a glance. (The title is also
|
|
304
|
+
# a clickable dashboard link.)
|
|
305
|
+
link_parts: list[str] = []
|
|
306
|
+
if alert_data.dashboard_url:
|
|
307
|
+
link_parts.append(self._link_markup(alert_data.dashboard_url, "Dashboard"))
|
|
308
|
+
for label, url in alert_data.links.items():
|
|
309
|
+
link_parts.append(self._link_markup(url, label))
|
|
310
|
+
if ctx["help_url"]:
|
|
311
|
+
link_parts.append(self._link_markup(ctx["help_url"], ctx["help_label"]))
|
|
312
|
+
links_line = line("Links", " · ".join(link_parts)) if link_parts else ""
|
|
313
|
+
|
|
314
|
+
# The body is built as sections joined by a blank line; the verbose tail
|
|
315
|
+
# is last so the platform fold hides it first.
|
|
316
|
+
sections: list[list[str]] = []
|
|
300
317
|
if kind == "anomaly":
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
"Quorum",
|
|
309
|
-
f"{ctx['detector_count']}/{ctx['min_detectors']} · {ctx['direction']}",
|
|
318
|
+
sections.append([ctx["anomaly_lead"], rule_chip])
|
|
319
|
+
sections.append(
|
|
320
|
+
[
|
|
321
|
+
line("Value", code(ctx["value_display"])),
|
|
322
|
+
line("Expected", code(ctx["expected_range"])),
|
|
323
|
+
links_line,
|
|
324
|
+
]
|
|
310
325
|
)
|
|
311
|
-
|
|
326
|
+
tail = [
|
|
327
|
+
line(
|
|
328
|
+
"Quorum",
|
|
329
|
+
f"{ctx['detector_count']}/{ctx['min_detectors']} · {ctx['direction']}",
|
|
330
|
+
),
|
|
331
|
+
line("Severity", f"{alert_data.severity:.2f}"),
|
|
332
|
+
]
|
|
312
333
|
if ctx["started_display"]:
|
|
313
|
-
|
|
314
|
-
|
|
334
|
+
tail.append(line("Anomaly began", ctx["started_display"]))
|
|
335
|
+
tail.append(line("Latest reading", ctx["timestamp"]))
|
|
315
336
|
else:
|
|
316
|
-
|
|
317
|
-
|
|
337
|
+
tail.append(line("Detected at", ctx["timestamp"]))
|
|
338
|
+
tail.append(line("Detectors", code(ctx["detector_name"])))
|
|
318
339
|
if ctx["detector_params"]:
|
|
319
340
|
# Fenced block on its own lines so it renders as a code block (and
|
|
320
341
|
# adds line breaks that help trip the platform's fold).
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
)
|
|
342
|
+
tail.append(f"{self._bold('Parameters')}\n```\n{ctx['detector_params']}\n```")
|
|
343
|
+
sections.append(tail)
|
|
324
344
|
elif kind == "recovery":
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
345
|
+
sections.append([ctx["recovery_lead"], rule_chip])
|
|
346
|
+
sections.append(
|
|
347
|
+
[
|
|
348
|
+
line("Value", code(ctx["value_display"])),
|
|
349
|
+
line("Expected", code(ctx["expected_range"])),
|
|
350
|
+
links_line,
|
|
351
|
+
]
|
|
352
|
+
)
|
|
353
|
+
# The incident timeline (onset → fired → recovered) + detectors;
|
|
354
|
+
# ``line`` drops the fired entry when unknown.
|
|
330
355
|
if ctx["started_display"]:
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
356
|
+
tail = [
|
|
357
|
+
line("Anomaly began", ctx["started_display"]),
|
|
358
|
+
line("Alert fired", ctx["fired_display"]),
|
|
359
|
+
line("Recovered", ctx["timestamp"]),
|
|
360
|
+
]
|
|
334
361
|
else:
|
|
335
|
-
|
|
336
|
-
|
|
362
|
+
tail = [line("Cleared at", ctx["timestamp"])]
|
|
363
|
+
tail.append(line("Detectors", code(ctx["detector_name"])))
|
|
364
|
+
sections.append(tail)
|
|
337
365
|
elif kind == "no_data":
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
366
|
+
sections.append(["Query returned no datapoint for the latest expected interval."])
|
|
367
|
+
sections.append(
|
|
368
|
+
[
|
|
369
|
+
line("Expected at", ctx["timestamp"]),
|
|
370
|
+
line("Expected", code(ctx["expected_range"])),
|
|
371
|
+
links_line,
|
|
372
|
+
]
|
|
373
|
+
)
|
|
341
374
|
else: # error
|
|
342
|
-
lead = "The detectkit pipeline failed for this metric."
|
|
343
|
-
full("Detected at", ctx["timestamp"])
|
|
344
375
|
err = f"{ctx['error_type']}: {ctx['error_message']}".strip(": ")
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if ctx["help_url"]:
|
|
360
|
-
link_parts.append(self._link_markup(ctx["help_url"], ctx["help_label"]))
|
|
361
|
-
if link_parts:
|
|
362
|
-
full("Links", " · ".join(link_parts))
|
|
376
|
+
sections.append(["The detectkit pipeline failed for this metric."])
|
|
377
|
+
sections.append(
|
|
378
|
+
[
|
|
379
|
+
line("Detected at", ctx["timestamp"]),
|
|
380
|
+
line("Error", code(err)),
|
|
381
|
+
links_line,
|
|
382
|
+
]
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
text = "\n\n".join(
|
|
386
|
+
"\n".join(entry for entry in section if entry)
|
|
387
|
+
for section in sections
|
|
388
|
+
if any(entry for entry in section)
|
|
389
|
+
)
|
|
363
390
|
|
|
364
391
|
# A plain-text one-liner for notification previews / unsupported clients.
|
|
365
392
|
if kind == "no_data":
|
|
@@ -372,26 +399,15 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
372
399
|
f"(expected {ctx['expected_range']}) at {ctx['timestamp']}"
|
|
373
400
|
)
|
|
374
401
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
# Detail card: neutral (no color bar) so it reads as a continuation of
|
|
385
|
-
# the base rather than a second alert, with the verbose tail in one
|
|
386
|
-
# foldable ``text`` block.
|
|
387
|
-
if detail_lines:
|
|
388
|
-
attachments.append(
|
|
389
|
-
{
|
|
390
|
-
"text": "\n".join(detail_lines),
|
|
391
|
-
"mrkdwn_in": ["text"],
|
|
392
|
-
}
|
|
393
|
-
)
|
|
394
|
-
return attachments
|
|
402
|
+
return [
|
|
403
|
+
{
|
|
404
|
+
"fallback": fallback,
|
|
405
|
+
"color": color,
|
|
406
|
+
"title": title,
|
|
407
|
+
"text": text,
|
|
408
|
+
"mrkdwn_in": ["text"],
|
|
409
|
+
}
|
|
410
|
+
]
|
|
395
411
|
|
|
396
412
|
def _bold(self, text: str) -> str:
|
|
397
413
|
"""Render *text* bold in the target platform's markdown.
|
|
@@ -36,6 +36,7 @@ class _OrchestratorBase:
|
|
|
36
36
|
links: dict[str, str] | None = None,
|
|
37
37
|
project_name: str | None = None,
|
|
38
38
|
help_url: str | None = None,
|
|
39
|
+
ai_synonyms: list[str] | None = None,
|
|
39
40
|
):
|
|
40
41
|
self.metric_name = metric_name
|
|
41
42
|
self.interval = interval
|
|
@@ -57,6 +58,10 @@ class _OrchestratorBase:
|
|
|
57
58
|
# defaulting to the official docs; None when opted out). Stamped onto every
|
|
58
59
|
# AlertData so channels render a guide link for non-operator stakeholders.
|
|
59
60
|
self.help_url = help_url
|
|
61
|
+
# OSI ai_context synonyms (the metric's alternative names). Stamped onto
|
|
62
|
+
# every AlertData so channels can render an "Also known as" identity line.
|
|
63
|
+
# Empty list when the metric has no ai_context — renders unchanged.
|
|
64
|
+
self.ai_synonyms = ai_synonyms or []
|
|
60
65
|
|
|
61
66
|
@staticmethod
|
|
62
67
|
def _group_by_timestamp(
|
|
@@ -280,6 +280,7 @@ class _DecisionMixin(_OrchestratorBase):
|
|
|
280
280
|
consecutive_count=consecutive_count,
|
|
281
281
|
description=self.description,
|
|
282
282
|
mentions=self.mentions,
|
|
283
|
+
ai_synonyms=self.ai_synonyms,
|
|
283
284
|
dashboard_url=self.dashboard_url,
|
|
284
285
|
links=self.links,
|
|
285
286
|
project_name=self.project_name,
|
|
@@ -345,6 +346,7 @@ class _DecisionMixin(_OrchestratorBase):
|
|
|
345
346
|
is_no_data=True,
|
|
346
347
|
description=self.description,
|
|
347
348
|
mentions=self.mentions,
|
|
349
|
+
ai_synonyms=self.ai_synonyms,
|
|
348
350
|
dashboard_url=self.dashboard_url,
|
|
349
351
|
links=self.links,
|
|
350
352
|
project_name=self.project_name,
|
|
@@ -188,6 +188,7 @@ class _RecoveryMixin(_OrchestratorBase):
|
|
|
188
188
|
is_recovery=True,
|
|
189
189
|
description=self.description,
|
|
190
190
|
mentions=self.mentions,
|
|
191
|
+
ai_synonyms=self.ai_synonyms,
|
|
191
192
|
dashboard_url=self.dashboard_url,
|
|
192
193
|
links=self.links,
|
|
193
194
|
project_name=self.project_name,
|
|
@@ -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
|
+
| OSI (Open Semantic Interchange) interop: import an OSI metric into a native metric / export back (`dtk osi`) | `.claude/rules/detectkit/cli.md` |
|
|
43
44
|
|
|
44
45
|
### Skills
|
|
45
46
|
|
|
@@ -167,7 +167,7 @@ config needed. Control it project-wide with `alert_help_url` in
|
|
|
167
167
|
Per-channel rendering (defaults only; the resolved help URL is rendered per channel as follows):
|
|
168
168
|
|
|
169
169
|
- **Slack / Mattermost / generic webhook** — a clickable `How to read this alert`
|
|
170
|
-
label in the compact `Links`
|
|
170
|
+
label in the compact `Links` line (alongside `Dashboard` + any extra links),
|
|
171
171
|
never a raw URL. Rendered in the platform's link syntax (Slack `<url|label>`,
|
|
172
172
|
Mattermost/generic markdown links) so a long dashboard URL stays hidden behind
|
|
173
173
|
its label.
|
|
@@ -191,25 +191,25 @@ leads with a colored **status circle** — 🔴 anomaly, 🟢 recovery, 🟡 no-
|
|
|
191
191
|
**project name** as a `[name] ` prefix (from `detectkit_project.yml`) — see
|
|
192
192
|
[Project label](#project-label-multi-project-channels) below.
|
|
193
193
|
|
|
194
|
-
- **Slack / Mattermost / generic webhook** — an
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
194
|
+
- **Slack / Mattermost / generic webhook** — an alert renders as **one
|
|
195
|
+
status-colored attachment** whose whole body is a single markdown `text` block,
|
|
196
|
+
ordered **most-important-first** so a long alert folds its tail behind a
|
|
197
|
+
**"Show more"** toggle — one block, one color, just like a reference
|
|
198
|
+
AlertManager alert. The body order: the clickable title (the metric; links to
|
|
199
|
+
`dashboard_url` when set), then the markdown lead (the duration sentence — see
|
|
200
|
+
"Incident timing" below) with the **Rule** chip beneath it, **Value /
|
|
201
|
+
Expected**, the compact **Links** line (dashboard + extra links + the "how to
|
|
202
|
+
read this alert" guide as clickable labels, never raw URLs), and finally the
|
|
203
|
+
verbose tail (Quorum / Severity / the anomalous span — Anomaly began → Latest
|
|
204
|
+
reading; began → fired → recovered on recovery — / Detectors / Parameters).
|
|
205
|
+
Both clients fold **only** the `text` (Slack above 700 chars / 5 line breaks;
|
|
206
|
+
Mattermost above ~200px) and render the title, the color bar and the **footer**
|
|
207
|
+
*outside* the fold — so the branded **footer + footer icon (the logo)** stays
|
|
208
|
+
visible even when the body is collapsed. No-data / error stay short, single
|
|
209
|
+
un-folded cards; a long anomaly (or a full recovery timeline) folds its tail.
|
|
210
|
+
@mentions ride in the **top-level** message text so they notify. A custom
|
|
211
|
+
`template` renders as a single plain text-only attachment (the raw template
|
|
212
|
+
replaces the structured lead/Value/tail sections; color/title/branding kept).
|
|
213
213
|
- **Telegram** — default `parse_mode` is now **HTML**. The default message is
|
|
214
214
|
structured and HTML-escaped: a colored status dot (red anomaly / green
|
|
215
215
|
recovery / yellow no-data / blue error), a bold headline, the lead + rule, then
|
|
@@ -297,6 +297,7 @@ referenced by path). Key variables:
|
|
|
297
297
|
| Variable | Meaning |
|
|
298
298
|
|---|---|
|
|
299
299
|
| `{metric_name}`, `{description}` / `{description_line}` | identity |
|
|
300
|
+
| `{synonyms}` / `{synonyms_line}` | alt names from `ai_context.synonyms` (opt-in; not in default templates — add `{synonyms_line}` for an "Also known as: …" line) |
|
|
300
301
|
| `{project_name}` / `{project_name_prefix}` | project label (`"[name] "` prefix, or `""`) |
|
|
301
302
|
| `{timestamp}`, `{timezone}` | when (display tz via `alerting.timezone`, default UTC) |
|
|
302
303
|
| `{value}` / `{value_display}` | metric value (`value_display` is NaN-safe) |
|
|
@@ -15,6 +15,8 @@ Run all commands from a project directory (the one containing
|
|
|
15
15
|
| `dtk test-alert <metric>` | Send a mock alert to the metric's channels |
|
|
16
16
|
| `dtk unlock --select <sel>` | Clear a stuck pipeline lock |
|
|
17
17
|
| `dtk clean --select <sel>` | Prune internal data that no longer matches the config |
|
|
18
|
+
| `dtk osi import <model>` | Scaffold a native metric from an OSI semantic-model metric (see "OSI interop") |
|
|
19
|
+
| `dtk osi export` | Publish metrics into an OSI fragment (config in `custom_extensions[detectkit]`) |
|
|
18
20
|
| `dtk --version` | Show installed detectkit version |
|
|
19
21
|
|
|
20
22
|
## Selectors (`--select` / `-s`)
|
|
@@ -193,6 +195,44 @@ actually delete.
|
|
|
193
195
|
unless `-y/--yes`; refuses to run if the project defines no metrics or configs
|
|
194
196
|
fail to parse (so a wrong directory can't wipe valid data).
|
|
195
197
|
|
|
198
|
+
## OSI interop (`dtk osi`)
|
|
199
|
+
|
|
200
|
+
`dtk osi` converts between [Open Semantic Interchange](https://github.com/open-semantic-interchange/OSI)
|
|
201
|
+
(OSI) semantic models and native detectkit metrics. It's a **separate, additive**
|
|
202
|
+
layer: it never runs the pipeline, takes no lock, and the converter package
|
|
203
|
+
(`detectkit/semantic/`) is not imported by load/detect/alert — so it can't affect
|
|
204
|
+
a running project. OSI is treated as an *interchange* format (define the KPI once,
|
|
205
|
+
consume in BI + AI), not an execution engine. OSI adoption is still early, so the
|
|
206
|
+
broadly-useful piece today is `ai_context` (see the metrics rule) — the converters
|
|
207
|
+
are a forward bridge for teams that already run a governed semantic layer.
|
|
208
|
+
|
|
209
|
+
- `dtk osi import <model.osi.yml> --metric <name> --interval <grain>` — the
|
|
210
|
+
"enhanced init": resolve one OSI metric and **scaffold a normal native metric**
|
|
211
|
+
(SQL `query`, interval, a starter detector, the metric's `ai_context`). Review
|
|
212
|
+
it, then commit like any hand-written metric. Targets:
|
|
213
|
+
- `--target clickhouse` (default) — a direct `toStartOfInterval(...) GROUP BY`
|
|
214
|
+
query from the dataset's physical `source` (ANSI→ClickHouse via **sqlglot**,
|
|
215
|
+
the optional `[osi]` extra: `pip install 'detectkit[osi]'`).
|
|
216
|
+
- `--target cube --cube <name> --time-field <dim>` — a Cube **SQL-API**
|
|
217
|
+
`MEASURE(...)` query, so the metric runs through Cube and the alert matches a
|
|
218
|
+
Cube dashboard's number by construction. Point its `profile` at a Postgres
|
|
219
|
+
connection on Cube's SQL port.
|
|
220
|
+
- Only provably per-bucket-additive measures compile (`SUM`/`COUNT`/`COUNT(DISTINCT)`/
|
|
221
|
+
`AVG`/`MIN`/`MAX` + ratios like `SUM(x)/NULLIF(COUNT(DISTINCT y),0)`). Window
|
|
222
|
+
functions / non-aggregates / unknown aggregates are **refused** with a message
|
|
223
|
+
to use `query_file:` — never a plausible-but-wrong series.
|
|
224
|
+
- `dtk osi compile <model> --metric <name> --interval <g>` prints just the SQL
|
|
225
|
+
(for review). `dtk osi export [--select <sel>]` writes an OSI fragment carrying
|
|
226
|
+
a lossless snapshot of the config in `custom_extensions[detectkit]` + the
|
|
227
|
+
`ai_context` — a **one-way carrier** (`import` does not reconstruct from it; the
|
|
228
|
+
metric YAML stays the source of truth).
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
dtk osi compile model.osi.yml -m total_sales -i 1h # preview the SQL
|
|
232
|
+
dtk osi import model.osi.yml -m total_sales -i 1h -o metrics/ # scaffold a metric
|
|
233
|
+
dtk osi export -o semantic/detectkit.osi.yml # publish back to OSI
|
|
234
|
+
```
|
|
235
|
+
|
|
196
236
|
## Common workflows
|
|
197
237
|
|
|
198
238
|
```bash
|