detectkit 0.56.2__tar.gz → 0.58.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.56.2/detectkit.egg-info → detectkit-0.58.0}/PKG-INFO +3 -1
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/__init__.py +1 -1
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/channels/webhook.py +273 -7
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/CLAUDE.section.md +1 -1
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/rules/cli.md +28 -1
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/rules/project.md +23 -3
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +15 -11
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +6 -3
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/commands/autotune.py +42 -19
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/commands/clean.py +27 -16
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/commands/init.py +11 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/commands/run.py +235 -18
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/main.py +18 -4
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/config/profile.py +13 -11
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/mysql_manager.py +47 -2
- detectkit-0.58.0/detectkit/tuning/assets/tune.js +187 -0
- {detectkit-0.56.2 → detectkit-0.58.0/detectkit.egg-info}/PKG-INFO +3 -1
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit.egg-info/requires.txt +3 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/pyproject.toml +3 -0
- detectkit-0.56.2/detectkit/tuning/assets/tune.js +0 -187
- {detectkit-0.56.2 → detectkit-0.58.0}/LICENSE +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/MANIFEST.in +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/README.md +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/axis_spec.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/commands/osi.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/commands/ui.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/config/metric_io.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/core/models.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/detectors/statistical/autoreg.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/semantic/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/semantic/errors.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/semantic/exporter.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/semantic/importer.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/semantic/osi_model.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/semantic/query_gen.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/tuning/payload.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/ui/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/ui/assets/ui.js +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/ui/html.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/ui/jobs.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/ui/metric_files.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/ui/overview.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/ui/server.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/requirements.txt +0 -0
- {detectkit-0.56.2 → detectkit-0.58.0}/setup.cfg +0 -0
- {detectkit-0.56.2 → detectkit-0.58.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.58.0
|
|
4
4
|
Summary: Metric monitoring with automatic anomaly detection
|
|
5
5
|
Author: detectkit team
|
|
6
6
|
License: MIT
|
|
@@ -36,6 +36,8 @@ Provides-Extra: postgres
|
|
|
36
36
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "postgres"
|
|
37
37
|
Provides-Extra: mysql
|
|
38
38
|
Requires-Dist: pymysql>=1.0.0; extra == "mysql"
|
|
39
|
+
Provides-Extra: mariadb
|
|
40
|
+
Requires-Dist: pymysql>=1.0.0; extra == "mariadb"
|
|
39
41
|
Provides-Extra: all-db
|
|
40
42
|
Requires-Dist: clickhouse-driver>=0.2.0; extra == "all-db"
|
|
41
43
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all-db"
|
|
@@ -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.58.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -5,6 +5,10 @@ Sends alerts to any webhook endpoint that accepts JSON payload.
|
|
|
5
5
|
Compatible with Mattermost, Slack, and other webhook-based systems.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import hashlib
|
|
9
|
+
import hmac
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
8
12
|
from datetime import datetime, timezone
|
|
9
13
|
from typing import Any
|
|
10
14
|
|
|
@@ -83,6 +87,17 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
83
87
|
channel (str): Target channel (optional, for Slack/Mattermost)
|
|
84
88
|
timeout (int): Request timeout in seconds (default: 10)
|
|
85
89
|
extra_headers (dict): Additional HTTP headers (optional)
|
|
90
|
+
format (str): Payload shape — "attachments" (default; today's
|
|
91
|
+
byte-identical Slack/Mattermost card), "json" (a structured event
|
|
92
|
+
payload, ``schema_version`` 1) or "alertmanager" (a Prometheus
|
|
93
|
+
Alertmanager webhook-receiver payload, version "4"). Any other
|
|
94
|
+
value raises ``ValueError`` at construction. A custom ``template``
|
|
95
|
+
passed to :meth:`send` is ignored for "json"/"alertmanager" —
|
|
96
|
+
those are structured formats with no free-text body.
|
|
97
|
+
secret (str): When set, every outgoing request (any format) is
|
|
98
|
+
HMAC-SHA256 signed GitHub-webhook style: header
|
|
99
|
+
``X-Detectkit-Signature-256: sha256=<hexdigest>`` computed over
|
|
100
|
+
the exact request body bytes with *secret* as the key.
|
|
86
101
|
|
|
87
102
|
Example:
|
|
88
103
|
>>> # Mattermost
|
|
@@ -103,6 +118,8 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
103
118
|
... )
|
|
104
119
|
"""
|
|
105
120
|
|
|
121
|
+
_FORMATS = ("attachments", "json", "alertmanager")
|
|
122
|
+
|
|
106
123
|
def __init__(
|
|
107
124
|
self,
|
|
108
125
|
webhook_url: str,
|
|
@@ -112,10 +129,15 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
112
129
|
channel: str | None = None,
|
|
113
130
|
timeout: int = 10,
|
|
114
131
|
extra_headers: dict[str, str] | None = None,
|
|
132
|
+
format: str = "attachments",
|
|
133
|
+
secret: str | None = None,
|
|
115
134
|
):
|
|
116
135
|
"""Initialize webhook channel."""
|
|
117
136
|
if not webhook_url:
|
|
118
137
|
raise ValueError("webhook_url is required")
|
|
138
|
+
if format not in self._FORMATS:
|
|
139
|
+
allowed = ", ".join(self._FORMATS)
|
|
140
|
+
raise ValueError(f"Unknown webhook format: '{format}'. Allowed: {allowed}")
|
|
119
141
|
|
|
120
142
|
self.webhook_url = webhook_url
|
|
121
143
|
self.username = username
|
|
@@ -129,6 +151,8 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
129
151
|
self.channel = channel
|
|
130
152
|
self.timeout = timeout
|
|
131
153
|
self.extra_headers = extra_headers or {}
|
|
154
|
+
self.format = format
|
|
155
|
+
self.secret = secret
|
|
132
156
|
|
|
133
157
|
def send(
|
|
134
158
|
self,
|
|
@@ -140,10 +164,12 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
140
164
|
|
|
141
165
|
Args:
|
|
142
166
|
alert_data: Alert data to send
|
|
143
|
-
template: Optional custom message template.
|
|
144
|
-
|
|
145
|
-
text (in place of the
|
|
146
|
-
otherwise the rich
|
|
167
|
+
template: Optional custom message template. Applies only to the
|
|
168
|
+
default "attachments" format, where the attachment carries the
|
|
169
|
+
formatted template verbatim as the body text (in place of the
|
|
170
|
+
structured lead/Value/tail sections); otherwise the rich
|
|
171
|
+
default is built. Ignored for "json"/"alertmanager" — those
|
|
172
|
+
are structured formats with no free-text body.
|
|
147
173
|
|
|
148
174
|
Returns:
|
|
149
175
|
True if sent successfully, False otherwise
|
|
@@ -155,17 +181,31 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
155
181
|
>>> channel = WebhookChannel(webhook_url="https://...")
|
|
156
182
|
>>> success = channel.send(alert_data)
|
|
157
183
|
"""
|
|
158
|
-
|
|
184
|
+
if self.format == "json":
|
|
185
|
+
payload = self.build_event_payload(alert_data)
|
|
186
|
+
elif self.format == "alertmanager":
|
|
187
|
+
payload = self.build_alertmanager_payload(alert_data)
|
|
188
|
+
else:
|
|
189
|
+
payload = self.build_payload(alert_data, template)
|
|
159
190
|
|
|
160
|
-
#
|
|
191
|
+
# Serialize ourselves (rather than letting ``requests`` do it via
|
|
192
|
+
# ``json=``) so the HMAC signature covers the exact bytes sent.
|
|
193
|
+
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
161
194
|
headers = {"Content-Type": "application/json"}
|
|
162
195
|
headers.update(self.extra_headers)
|
|
196
|
+
if self.format in ("json", "alertmanager"):
|
|
197
|
+
headers["X-Detectkit-Event"] = self.status_kind(alert_data)
|
|
198
|
+
if self.secret:
|
|
199
|
+
# GitHub-webhook style signature, computed AFTER extra_headers so
|
|
200
|
+
# a misconfigured extra header can't clobber it.
|
|
201
|
+
signature = hmac.new(self.secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
|
|
202
|
+
headers["X-Detectkit-Signature-256"] = f"sha256={signature}"
|
|
163
203
|
|
|
164
204
|
# Send to webhook
|
|
165
205
|
try:
|
|
166
206
|
response = requests.post(
|
|
167
207
|
self.webhook_url,
|
|
168
|
-
|
|
208
|
+
data=body,
|
|
169
209
|
headers=headers,
|
|
170
210
|
timeout=self.timeout,
|
|
171
211
|
)
|
|
@@ -405,6 +445,232 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
405
445
|
}
|
|
406
446
|
]
|
|
407
447
|
|
|
448
|
+
def _display_lead(self, alert_data: AlertData, ctx: dict[str, Any], kind: str) -> str:
|
|
449
|
+
"""The plain-language lead sentence shared by both structured formats.
|
|
450
|
+
|
|
451
|
+
Anomaly/recovery reuse the same ready-made sentence the attachment
|
|
452
|
+
body leads with; no-data/error get a fixed one-liner (they have no
|
|
453
|
+
streak/duration story).
|
|
454
|
+
"""
|
|
455
|
+
if kind == "anomaly":
|
|
456
|
+
return str(ctx["anomaly_lead"])
|
|
457
|
+
if kind == "recovery":
|
|
458
|
+
return str(ctx["recovery_lead"])
|
|
459
|
+
if kind == "no_data":
|
|
460
|
+
return "Query returned no datapoint for the latest expected interval."
|
|
461
|
+
return "The detectkit pipeline failed for this metric."
|
|
462
|
+
|
|
463
|
+
def build_event_payload(self, alert_data: AlertData) -> dict[str, Any]:
|
|
464
|
+
"""Structured JSON event payload (``format="json"``).
|
|
465
|
+
|
|
466
|
+
``schema_version`` 1 — **frozen**: this shape is a public contract for
|
|
467
|
+
downstream consumers (custom receivers, log pipelines), so existing
|
|
468
|
+
keys must not change meaning or disappear; only additive changes are
|
|
469
|
+
safe without bumping the version. Built from ``alert_data`` plus
|
|
470
|
+
:meth:`build_context` (the same resolved values every other rendering
|
|
471
|
+
surface — the attachment, Telegram, email — uses), so this event and
|
|
472
|
+
the human-readable message never disagree.
|
|
473
|
+
"""
|
|
474
|
+
ctx = self.build_context(alert_data)
|
|
475
|
+
kind = self.status_kind(alert_data)
|
|
476
|
+
streak = alert_data.consecutive_count
|
|
477
|
+
interval_seconds = alert_data.interval_seconds
|
|
478
|
+
duration_seconds = streak * interval_seconds if interval_seconds is not None else None
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
"schema_version": 1,
|
|
482
|
+
"source": "detectkit",
|
|
483
|
+
"kind": kind,
|
|
484
|
+
"status": "resolved" if alert_data.is_recovery else "firing",
|
|
485
|
+
"project": alert_data.project_name,
|
|
486
|
+
"metric": alert_data.metric_name,
|
|
487
|
+
"description": alert_data.description,
|
|
488
|
+
"timestamp": self._iso_utc(alert_data.timestamp),
|
|
489
|
+
"value": self._clean_float(alert_data.value),
|
|
490
|
+
"expected": {
|
|
491
|
+
"lower": self._bounded_value(alert_data.confidence_lower),
|
|
492
|
+
"upper": self._bounded_value(alert_data.confidence_upper),
|
|
493
|
+
},
|
|
494
|
+
"severity": self._clean_float(alert_data.severity),
|
|
495
|
+
"direction": alert_data.direction,
|
|
496
|
+
"detector": {
|
|
497
|
+
"name": alert_data.detector_name,
|
|
498
|
+
"params": self._parse_detector_params(alert_data.detector_params),
|
|
499
|
+
},
|
|
500
|
+
"rule": {
|
|
501
|
+
"min_detectors": ctx["min_detectors"],
|
|
502
|
+
"direction": ctx["direction_policy"],
|
|
503
|
+
"consecutive": ctx["consecutive_required"],
|
|
504
|
+
"window_points": alert_data.window_points,
|
|
505
|
+
"min_anomaly_share": alert_data.min_anomaly_share,
|
|
506
|
+
"fired_by_share": alert_data.fired_by_share,
|
|
507
|
+
"display": ctx["rule_display"],
|
|
508
|
+
},
|
|
509
|
+
"quorum": {
|
|
510
|
+
"detector_count": alert_data.detector_count,
|
|
511
|
+
"min_detectors": ctx["min_detectors"],
|
|
512
|
+
},
|
|
513
|
+
"incident": {
|
|
514
|
+
"onset": self._iso_utc(alert_data.onset_timestamp),
|
|
515
|
+
"streak": streak,
|
|
516
|
+
"capped": alert_data.streak_capped,
|
|
517
|
+
"interval_seconds": interval_seconds,
|
|
518
|
+
"duration_seconds": duration_seconds,
|
|
519
|
+
},
|
|
520
|
+
"links": {
|
|
521
|
+
"dashboard": alert_data.dashboard_url,
|
|
522
|
+
"help": ctx["help_url"] or None,
|
|
523
|
+
"extra": alert_data.links,
|
|
524
|
+
},
|
|
525
|
+
"mentions": alert_data.mentions,
|
|
526
|
+
"synonyms": alert_data.ai_synonyms,
|
|
527
|
+
"error": (
|
|
528
|
+
{"type": alert_data.error_type, "message": alert_data.error_message}
|
|
529
|
+
if kind == "error"
|
|
530
|
+
else None
|
|
531
|
+
),
|
|
532
|
+
"display": {
|
|
533
|
+
"title": self.format_title(alert_data),
|
|
534
|
+
"lead": self._display_lead(alert_data, ctx, kind),
|
|
535
|
+
"value": ctx["value_display"],
|
|
536
|
+
"expected_range": ctx["expected_range"],
|
|
537
|
+
"timestamp": ctx["timestamp"],
|
|
538
|
+
},
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
def build_alertmanager_payload(self, alert_data: AlertData) -> dict[str, Any]:
|
|
542
|
+
"""Prometheus Alertmanager webhook-receiver payload (``format="alertmanager"``).
|
|
543
|
+
|
|
544
|
+
Version "4" — **frozen** shape, same contract rationale as
|
|
545
|
+
:meth:`build_event_payload`. A recovery deliberately carries the
|
|
546
|
+
**same labels** as the anomaly it resolves (``kind`` maps back to
|
|
547
|
+
``"anomaly"``) so Alertmanager-style consumers pair the two via
|
|
548
|
+
``fingerprint`` — only ``status``/``endsAt`` differ.
|
|
549
|
+
"""
|
|
550
|
+
ctx = self.build_context(alert_data)
|
|
551
|
+
kind = self.status_kind(alert_data)
|
|
552
|
+
is_recovery = alert_data.is_recovery
|
|
553
|
+
|
|
554
|
+
labels_kind = "anomaly" if is_recovery else kind
|
|
555
|
+
labels: dict[str, str] = {
|
|
556
|
+
"alertname": alert_data.metric_name,
|
|
557
|
+
"metric": alert_data.metric_name,
|
|
558
|
+
"kind": labels_kind,
|
|
559
|
+
"source": "detectkit",
|
|
560
|
+
"severity": "warning" if labels_kind == "no_data" else "critical",
|
|
561
|
+
}
|
|
562
|
+
if alert_data.project_name:
|
|
563
|
+
labels["project"] = alert_data.project_name
|
|
564
|
+
|
|
565
|
+
lead = self._display_lead(alert_data, ctx, kind)
|
|
566
|
+
annotations: dict[str, str] = {
|
|
567
|
+
"summary": self.format_title(alert_data),
|
|
568
|
+
"description": lead,
|
|
569
|
+
}
|
|
570
|
+
if kind in ("anomaly", "recovery"):
|
|
571
|
+
annotations["value"] = ctx["value_display"]
|
|
572
|
+
annotations["expected"] = ctx["expected_range"]
|
|
573
|
+
# Direction rides as an ANNOTATION, never a label: the orchestrator
|
|
574
|
+
# stamps recovery AlertData with direction="none", so a direction label
|
|
575
|
+
# would change the label set — and the fingerprint — between a firing
|
|
576
|
+
# alert and its recovery, breaking trigger/resolve pairing.
|
|
577
|
+
if kind == "anomaly" and alert_data.direction and alert_data.direction != "none":
|
|
578
|
+
annotations["direction"] = alert_data.direction
|
|
579
|
+
|
|
580
|
+
fingerprint = self._label_fingerprint(labels)
|
|
581
|
+
onset = (
|
|
582
|
+
alert_data.onset_timestamp
|
|
583
|
+
if alert_data.onset_timestamp is not None
|
|
584
|
+
else alert_data.timestamp
|
|
585
|
+
)
|
|
586
|
+
status = "resolved" if is_recovery else "firing"
|
|
587
|
+
|
|
588
|
+
alert = {
|
|
589
|
+
"status": status,
|
|
590
|
+
"labels": labels,
|
|
591
|
+
"annotations": annotations,
|
|
592
|
+
"startsAt": self._iso_utc(onset),
|
|
593
|
+
"endsAt": (
|
|
594
|
+
self._iso_utc(alert_data.timestamp) if is_recovery else "0001-01-01T00:00:00Z"
|
|
595
|
+
),
|
|
596
|
+
"generatorURL": alert_data.dashboard_url or "",
|
|
597
|
+
"fingerprint": fingerprint,
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
"version": "4",
|
|
602
|
+
"groupKey": f"detectkit/{alert_data.project_name or 'default'}/{alert_data.metric_name}",
|
|
603
|
+
"truncatedAlerts": 0,
|
|
604
|
+
"status": status,
|
|
605
|
+
"receiver": "detectkit",
|
|
606
|
+
"groupLabels": {"alertname": alert_data.metric_name},
|
|
607
|
+
"commonLabels": labels,
|
|
608
|
+
"commonAnnotations": annotations,
|
|
609
|
+
"externalURL": "",
|
|
610
|
+
"alerts": [alert],
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
@staticmethod
|
|
614
|
+
def _label_fingerprint(labels: dict[str, str]) -> str:
|
|
615
|
+
"""Stable short hash of *labels*, sorted so key order never matters."""
|
|
616
|
+
label_str = "\n".join(f"{k}={v}" for k, v in sorted(labels.items()))
|
|
617
|
+
return hashlib.sha256(label_str.encode("utf-8")).hexdigest()[:16]
|
|
618
|
+
|
|
619
|
+
@staticmethod
|
|
620
|
+
def _clean_float(value: Any) -> float | None:
|
|
621
|
+
"""``float(value)``, or ``None`` for a missing/NaN value."""
|
|
622
|
+
if value is None:
|
|
623
|
+
return None
|
|
624
|
+
if isinstance(value, float) and math.isnan(value):
|
|
625
|
+
return None
|
|
626
|
+
return float(value)
|
|
627
|
+
|
|
628
|
+
@staticmethod
|
|
629
|
+
def _bounded_value(bound: Any) -> float | None:
|
|
630
|
+
"""A confidence bound as JSON, or ``None`` when unset/NaN/inf.
|
|
631
|
+
|
|
632
|
+
Mirrors ``BaseAlertChannel.build_context``'s ``_bounded`` check (a
|
|
633
|
+
NaN/inf bound means "no bound on that side", e.g. a one-sided
|
|
634
|
+
ManualBounds detector).
|
|
635
|
+
"""
|
|
636
|
+
if bound is None or (isinstance(bound, float) and (math.isnan(bound) or math.isinf(bound))):
|
|
637
|
+
return None
|
|
638
|
+
return float(bound)
|
|
639
|
+
|
|
640
|
+
@staticmethod
|
|
641
|
+
def _parse_detector_params(raw: str) -> Any:
|
|
642
|
+
"""Detector params as parsed JSON when possible, else the raw string.
|
|
643
|
+
|
|
644
|
+
``detector_params`` is stored as a JSON string; consumers of the
|
|
645
|
+
structured formats want it as real JSON, not a string-within-a-string
|
|
646
|
+
— but a non-JSON/non-container value (or an empty string) is passed
|
|
647
|
+
through rather than dropped.
|
|
648
|
+
"""
|
|
649
|
+
if not raw:
|
|
650
|
+
return None
|
|
651
|
+
try:
|
|
652
|
+
parsed = json.loads(raw)
|
|
653
|
+
except (ValueError, TypeError):
|
|
654
|
+
return raw
|
|
655
|
+
return parsed if isinstance(parsed, dict | list) else raw
|
|
656
|
+
|
|
657
|
+
@staticmethod
|
|
658
|
+
def _iso_utc(ts: Any) -> str | None:
|
|
659
|
+
"""ISO-8601 UTC string (``2026-07-11T10:30:00Z``) for a naive-UTC
|
|
660
|
+
timestamp, or ``None`` when unset/unparseable."""
|
|
661
|
+
if ts is None:
|
|
662
|
+
return None
|
|
663
|
+
try:
|
|
664
|
+
if isinstance(ts, np.datetime64):
|
|
665
|
+
ts = ts.astype("datetime64[s]").astype(datetime)
|
|
666
|
+
if isinstance(ts, datetime):
|
|
667
|
+
if ts.tzinfo is not None:
|
|
668
|
+
ts = ts.astimezone(timezone.utc).replace(tzinfo=None)
|
|
669
|
+
return str(ts.strftime("%Y-%m-%dT%H:%M:%SZ"))
|
|
670
|
+
except (ValueError, TypeError, OverflowError):
|
|
671
|
+
return None
|
|
672
|
+
return None
|
|
673
|
+
|
|
408
674
|
def _bold(self, text: str) -> str:
|
|
409
675
|
"""Render *text* bold in the target platform's markdown.
|
|
410
676
|
|
|
@@ -14,7 +14,7 @@ conventions.
|
|
|
14
14
|
**Database access for _you_ (recommended, not required).** detectkit itself
|
|
15
15
|
connects to the database directly via its drivers — it **never** needs an MCP to
|
|
16
16
|
run. But you assist far better with **read access to the same database** (e.g. a
|
|
17
|
-
database MCP for the project's ClickHouse / PostgreSQL / MySQL): you can inspect
|
|
17
|
+
database MCP for the project's ClickHouse / PostgreSQL / MySQL / MariaDB): you can inspect
|
|
18
18
|
a metric's series, find real incidents to label for `dtk autotune`, sanity-check
|
|
19
19
|
a metric query before running it, and confirm detections — instead of asking the
|
|
20
20
|
user to run every query by hand. Without it, fall back to marking incidents
|
|
@@ -37,11 +37,26 @@ Used by `run`, `unlock`, and `clean` (drift mode). Three forms:
|
|
|
37
37
|
(`--select "*" --exclude "metrics/staging/*"`). Metric names must be unique
|
|
38
38
|
across the project; duplicates raise an error listing the conflicting files.
|
|
39
39
|
|
|
40
|
+
## Exit codes
|
|
41
|
+
|
|
42
|
+
`dtk run`, `dtk autotune`, and `dtk clean` return a process exit code meant for
|
|
43
|
+
schedulers and CI to gate on:
|
|
44
|
+
|
|
45
|
+
| Code | Meaning |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `0` | Success — every metric completed the requested steps. |
|
|
48
|
+
| `1` | Failure — any metric failed, the run aborted early, a startup/config/DB error occurred, or the selector matched no metrics. |
|
|
49
|
+
| `2` | Usage error — bad CLI arguments. |
|
|
50
|
+
|
|
51
|
+
On `dtk autotune`, a metric that's disabled in its config counts as **skipped**,
|
|
52
|
+
not failed, and doesn't turn the exit code non-zero by itself. Other commands
|
|
53
|
+
keep Click's default behavior (0 on success, non-zero on any exception).
|
|
54
|
+
|
|
40
55
|
## `dtk run`
|
|
41
56
|
|
|
42
57
|
```bash
|
|
43
58
|
dtk run --select <sel> [--steps load,detect,alert] [--from DATE] [--to DATE] \
|
|
44
|
-
[--full-refresh] [--force] [--profile NAME] [--report [PATH]]
|
|
59
|
+
[--full-refresh] [--force] [--profile NAME] [--report [PATH]] [--json]
|
|
45
60
|
```
|
|
46
61
|
|
|
47
62
|
- `--steps` — which of `load`, `detect`, `alert` to run (default all); they always
|
|
@@ -66,6 +81,12 @@ dtk run --select <sel> [--steps load,detect,alert] [--from DATE] [--to DATE] \
|
|
|
66
81
|
tables, so even a `--steps load` run can produce one. Dual-mode: bare `--report`
|
|
67
82
|
→ `reports/<metric>.html`; `--report <dir>` → `<dir>/<metric>.html`;
|
|
68
83
|
`--report file.html` → that file.
|
|
84
|
+
- `--json` — human-readable progress still goes to **stderr**; **stdout** gets
|
|
85
|
+
exactly one JSON summary line for scripting/CI:
|
|
86
|
+
`{schema_version: 1, command, project, selector, steps, started_at,
|
|
87
|
+
finished_at, status: success|failed|error, metrics: [{name, status,
|
|
88
|
+
steps_completed, datapoints_loaded, anomalies_detected, alerts_sent, error}],
|
|
89
|
+
totals: {metrics, succeeded, failed, skipped, ...}, exit_code}`.
|
|
69
90
|
|
|
70
91
|
## `dtk autotune --select <sel>`
|
|
71
92
|
|
|
@@ -396,6 +417,12 @@ timers / Windows Task Scheduler. Always `cd` into the project first:
|
|
|
396
417
|
Pair scheduling with `error_alerting` (in `detectkit_project.yml`) so in-process
|
|
397
418
|
failures page someone; cron monitoring covers `dtk run` not running at all.
|
|
398
419
|
|
|
420
|
+
Beyond cron, orchestrators can gate directly on the process instead of parsing
|
|
421
|
+
logs: an Airflow `BashOperator`, a Dagster op, a Prefect task, or a CI job all
|
|
422
|
+
fail their step when `dtk run` / `dtk autotune` / `dtk clean` exits non-zero
|
|
423
|
+
(see "Exit codes" above), and can additionally parse `dtk run --json`'s stdout
|
|
424
|
+
summary for per-metric detail.
|
|
425
|
+
|
|
399
426
|
## Troubleshooting
|
|
400
427
|
|
|
401
428
|
- **"Metric not found"** — selector doesn't match. Use the bare name
|
|
@@ -116,9 +116,10 @@ alert_channels:
|
|
|
116
116
|
|
|
117
117
|
### Database profiles
|
|
118
118
|
|
|
119
|
-
> ClickHouse, PostgreSQL and
|
|
120
|
-
> two *databases*; PostgreSQL connects to one
|
|
121
|
-
> `
|
|
119
|
+
> ClickHouse, PostgreSQL, MySQL and MariaDB are all fully supported.
|
|
120
|
+
> ClickHouse/MySQL/MariaDB use two *databases*; PostgreSQL connects to one
|
|
121
|
+
> `database` and uses two *schemas*.
|
|
122
|
+
> `dtk init --db-type {clickhouse,postgres,mysql,mariadb}` scaffolds the right shape.
|
|
122
123
|
|
|
123
124
|
**ClickHouse**:
|
|
124
125
|
```yaml
|
|
@@ -166,6 +167,12 @@ profiles:
|
|
|
166
167
|
settings: {} # optional — extra pymysql.connect kwargs
|
|
167
168
|
```
|
|
168
169
|
|
|
170
|
+
**MariaDB**: identical fields to the MySQL block above — just spell `type:
|
|
171
|
+
mariadb` instead of `type: mysql`. The vendor (MySQL vs MariaDB) is
|
|
172
|
+
auto-detected at connect (`SELECT VERSION()`), so MariaDB gets its own
|
|
173
|
+
`VALUES()`-form upsert instead of MySQL 8.0.19's row-alias form.
|
|
174
|
+
`pip install 'detectkit[mariadb]'`.
|
|
175
|
+
|
|
169
176
|
### Alert channels
|
|
170
177
|
|
|
171
178
|
Defined once in `profiles.yml`, referenced by name in each metric's
|
|
@@ -228,9 +235,22 @@ alert_channels:
|
|
|
228
235
|
webhook_alerts:
|
|
229
236
|
type: webhook
|
|
230
237
|
webhook_url: "{{ env_var('WEBHOOK_URL') }}" # required
|
|
238
|
+
format: attachments # optional: attachments (default) | json | alertmanager
|
|
239
|
+
secret: "{{ env_var('WEBHOOK_SECRET') }}" # optional — HMAC-SHA256-signs the request body
|
|
231
240
|
extra_headers: # optional
|
|
232
241
|
Authorization: "Bearer {{ env_var('WEBHOOK_TOKEN') }}"
|
|
233
242
|
```
|
|
243
|
+
> `format` — `attachments` (default) renders the same platform-style card as
|
|
244
|
+
> Mattermost/Slack; `json` sends a versioned structured event
|
|
245
|
+
> (`schema_version: 1`; fields like `kind`/`status`/`project`/`metric`/
|
|
246
|
+
> `timestamp`/`value`/`expected`/`severity`/`direction`/`detector`/`rule`/
|
|
247
|
+
> `quorum`/`incident`/`links`/`display`); `alertmanager` sends a Prometheus
|
|
248
|
+
> Alertmanager webhook-receiver payload (v4) — a trigger/resolve pair sharing
|
|
249
|
+
> identical labels/fingerprint, `severity: critical` for anomaly/error and
|
|
250
|
+
> `warning` for no-data (no-data never auto-resolves). `json`/`alertmanager`
|
|
251
|
+
> ignore a custom `template` and add an `X-Detectkit-Event` header.
|
|
252
|
+
> `secret` — signs the raw request body as HMAC-SHA256 into
|
|
253
|
+
> `X-Detectkit-Signature-256: sha256=<hex>`, for any `format`.
|
|
234
254
|
|
|
235
255
|
## Notes
|
|
236
256
|
|
{detectkit-0.56.2 → detectkit-0.58.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md
RENAMED
|
@@ -50,17 +50,19 @@ side by side, ask which one to set up.
|
|
|
50
50
|
|
|
51
51
|
## Step 1 — Pick the database backend
|
|
52
52
|
|
|
53
|
-
**ClickHouse, PostgreSQL and
|
|
54
|
-
project uses (default to ClickHouse if unsure). `dtk init
|
|
55
|
-
{clickhouse,postgres,mysql}` scaffolds `profiles.yml` for the
|
|
56
|
-
The location fields differ:
|
|
57
|
-
- **ClickHouse** / **MySQL** — two *databases*: `internal_database` / `data_database`.
|
|
53
|
+
**ClickHouse, PostgreSQL, MySQL and MariaDB are all fully supported.** Ask
|
|
54
|
+
which one the project uses (default to ClickHouse if unsure). `dtk init
|
|
55
|
+
--db-type {clickhouse,postgres,mysql,mariadb}` scaffolds `profiles.yml` for the
|
|
56
|
+
chosen backend. The location fields differ:
|
|
57
|
+
- **ClickHouse** / **MySQL** / **MariaDB** — two *databases*: `internal_database` / `data_database`.
|
|
58
58
|
- **PostgreSQL** — connect to a `database` (must already exist), then two
|
|
59
59
|
*schemas*: `internal_schema` / `data_schema`.
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
`type: mariadb` is an identical profile alias to `type: mysql` (same location
|
|
62
|
+
fields; the vendor is auto-detected at connect). The metric query SQL dialect
|
|
63
|
+
also differs (e.g. `toStartOfInterval` on ClickHouse vs `date_trunc`/
|
|
64
|
+
`to_timestamp` on Postgres vs `FROM_UNIXTIME` on MySQL/MariaDB). Everything
|
|
65
|
+
else — detectors, alerting, the CLI — is identical.
|
|
64
66
|
|
|
65
67
|
## Step 2 — Connection details (gather, don't guess)
|
|
66
68
|
|
|
@@ -136,9 +138,11 @@ identity fields below are optional overrides.
|
|
|
136
138
|
- **email** — `smtp_host`, `smtp_port`, `from_email`, `to_emails` (list);
|
|
137
139
|
optional `from_name` (From display name, default `detectkit`);
|
|
138
140
|
`smtp_username` / `smtp_password` via env_var.
|
|
139
|
-
- **webhook** — generic `webhook_url` (required) + optional `
|
|
140
|
-
(
|
|
141
|
-
|
|
141
|
+
- **webhook** — generic `webhook_url` (required) + optional `format`
|
|
142
|
+
(`attachments` default / `json` / `alertmanager`), `secret` (HMAC-SHA256-signs
|
|
143
|
+
the request body into `X-Detectkit-Signature-256`), and `extra_headers` (also
|
|
144
|
+
accepts `username` / `icon_url` / `icon_emoji` / `channel`). There is no
|
|
145
|
+
`url`, `method` or `headers` field.
|
|
142
146
|
|
|
143
147
|
```yaml
|
|
144
148
|
# at the end of profiles.yml
|
|
@@ -114,9 +114,12 @@ Walk the user to the mode that fits what they want to do:
|
|
|
114
114
|
Z-Score / IQR, **Autoreg** = predicts each point from its previous values
|
|
115
115
|
(**Lags** knob; the windowed-only knobs hide), or **Manual** = fixed
|
|
116
116
|
lower/upper bounds), **threshold**, **window size**, **recency weighting +
|
|
117
|
-
half-life**, **detrend**, **smoothing**,
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
half-life**, **detrend**, **smoothing**, **seasonality groups**, and (for a
|
|
118
|
+
seasonal metric) **min samples per group** — how many same-key points the
|
|
119
|
+
window needs before a seasonal group gets its own band; lowering it keeps
|
|
120
|
+
seasonality active on a smaller window instead of silently falling back to the
|
|
121
|
+
global band. The band + anomalies + would-fire alerts recompute on every
|
|
122
|
+
change. The window-size / half-life readouts show the equivalent wall-clock span. **Stabilization**
|
|
120
123
|
(none / clamp) clamps a flagged point to
|
|
121
124
|
the bound it violated in later windows' statistics — try it when a sustained
|
|
122
125
|
incident visibly balloons the band mid-incident and the detector stops
|