detectkit 0.10.0__tar.gz → 0.12.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.10.0/detectkit.egg-info → detectkit-0.12.0}/PKG-INFO +4 -2
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/__init__.py +1 -1
- detectkit-0.12.0/detectkit/alerting/channels/branding.py +20 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/email.py +46 -2
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/mattermost.py +12 -4
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/slack.py +12 -4
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/webhook.py +28 -7
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/overview.md +3 -3
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/project.md +34 -22
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +22 -12
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/init.py +189 -97
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/main.py +11 -3
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/config/profile.py +39 -3
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/core/models.py +11 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/__init__.py +6 -0
- detectkit-0.12.0/detectkit/database/_sql_manager.py +398 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/clickhouse_manager.py +38 -16
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_alert_states.py +14 -29
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_datapoints.py +6 -5
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_detections.py +9 -11
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_maintenance.py +5 -7
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_schema.py +5 -1
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/manager.py +73 -0
- detectkit-0.12.0/detectkit/database/mysql_manager.py +132 -0
- detectkit-0.12.0/detectkit/database/postgres_manager.py +118 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/tables.py +3 -0
- {detectkit-0.10.0 → detectkit-0.12.0/detectkit.egg-info}/PKG-INFO +4 -2
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit.egg-info/SOURCES.txt +4 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit.egg-info/requires.txt +3 -1
- {detectkit-0.10.0 → detectkit-0.12.0}/pyproject.toml +5 -1
- {detectkit-0.10.0 → detectkit-0.12.0}/LICENSE +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/MANIFEST.in +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/README.md +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/requirements.txt +0 -0
- {detectkit-0.10.0 → detectkit-0.12.0}/setup.cfg +0 -0
- {detectkit-0.10.0 → detectkit-0.12.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.12.0
|
|
4
4
|
Summary: Metric monitoring with automatic anomaly detection
|
|
5
5
|
Author: detectkit team
|
|
6
6
|
License: MIT
|
|
@@ -61,7 +61,9 @@ Requires-Dist: black>=23.0; extra == "dev"
|
|
|
61
61
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
62
62
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
63
63
|
Provides-Extra: integration
|
|
64
|
-
Requires-Dist: testcontainers[clickhouse]>=4.0; extra == "integration"
|
|
64
|
+
Requires-Dist: testcontainers[clickhouse,mysql,postgres]>=4.0; extra == "integration"
|
|
65
|
+
Requires-Dist: psycopg2-binary>=2.9.0; extra == "integration"
|
|
66
|
+
Requires-Dist: pymysql>=1.0.0; extra == "integration"
|
|
65
67
|
Dynamic: license-file
|
|
66
68
|
|
|
67
69
|
# detectkit
|
|
@@ -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.12.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Default detectkit branding for alert channels.
|
|
3
|
+
|
|
4
|
+
Centralizes the bot identity — display name and avatar — so every channel
|
|
5
|
+
defaults to the **detectkit brand**. These are only the fallbacks: each channel
|
|
6
|
+
still accepts per-channel overrides (``username`` / ``from_name`` for the name,
|
|
7
|
+
``icon_url`` / ``icon_emoji`` for the avatar). Keeping them in one place means
|
|
8
|
+
the brand avatar URL and name have a single source of truth.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Bot display name shown as the message sender across channels (webhook-family
|
|
12
|
+
# ``username`` and the email ``From`` display name).
|
|
13
|
+
BRAND_USERNAME = "detectkit"
|
|
14
|
+
|
|
15
|
+
# Brand avatar served from the docs site. Slack/Mattermost render the webhook
|
|
16
|
+
# bot icon from a **raster** URL (an SVG is not rendered as a bot avatar), so
|
|
17
|
+
# this points at a PNG. Override per channel with ``icon_url`` (a custom image)
|
|
18
|
+
# or opt out of the avatar entirely with ``icon_emoji``. The PNG is generated by
|
|
19
|
+
# ``website/scripts/make-bot-icon.mjs`` and served from ``website/public/``.
|
|
20
|
+
BRAND_ICON_URL = "https://dtk.pipelab.dev/bot-icon.png"
|
|
@@ -4,11 +4,14 @@ Email alert channel implementation.
|
|
|
4
4
|
Sends anomaly alerts via SMTP email.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import html
|
|
7
8
|
import smtplib
|
|
8
9
|
from email.mime.multipart import MIMEMultipart
|
|
9
10
|
from email.mime.text import MIMEText
|
|
11
|
+
from email.utils import formataddr
|
|
10
12
|
|
|
11
13
|
from detectkit.alerting.channels.base import AlertData, BaseAlertChannel
|
|
14
|
+
from detectkit.alerting.channels.branding import BRAND_ICON_URL, BRAND_USERNAME
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
class EmailChannel(BaseAlertChannel):
|
|
@@ -55,6 +58,7 @@ class EmailChannel(BaseAlertChannel):
|
|
|
55
58
|
smtp_password: str | None = None,
|
|
56
59
|
use_tls: bool = True,
|
|
57
60
|
subject_template: str = "⚠ Alert: {metric_name}",
|
|
61
|
+
from_name: str = BRAND_USERNAME,
|
|
58
62
|
template: str | None = None,
|
|
59
63
|
**kwargs,
|
|
60
64
|
):
|
|
@@ -70,6 +74,9 @@ class EmailChannel(BaseAlertChannel):
|
|
|
70
74
|
smtp_password: SMTP authentication password (optional)
|
|
71
75
|
use_tls: Whether to use STARTTLS (default: True)
|
|
72
76
|
subject_template: Email subject template with {metric_name} placeholder
|
|
77
|
+
from_name: Sender display name shown in the ``From`` header — the
|
|
78
|
+
email equivalent of the bot name (default: "detectkit"). The
|
|
79
|
+
brand logo is also rendered in the HTML body.
|
|
73
80
|
template: Custom message template (optional)
|
|
74
81
|
**kwargs: Additional parameters (ignored)
|
|
75
82
|
|
|
@@ -93,6 +100,7 @@ class EmailChannel(BaseAlertChannel):
|
|
|
93
100
|
self.to_emails = to_emails
|
|
94
101
|
self.use_tls = use_tls
|
|
95
102
|
self.subject_template = subject_template
|
|
103
|
+
self.from_name = from_name
|
|
96
104
|
self.template = template
|
|
97
105
|
|
|
98
106
|
def send(self, alert_data: AlertData, template: str | None = None) -> bool:
|
|
@@ -117,12 +125,17 @@ class EmailChannel(BaseAlertChannel):
|
|
|
117
125
|
|
|
118
126
|
# Create email message
|
|
119
127
|
msg = MIMEMultipart("alternative")
|
|
120
|
-
|
|
128
|
+
# Branded From: "detectkit <alerts@example.com>" — the email equivalent
|
|
129
|
+
# of a bot display name. formataddr quotes the name when required.
|
|
130
|
+
msg["From"] = formataddr((self.from_name, self.from_email))
|
|
121
131
|
msg["To"] = ", ".join(self.to_emails)
|
|
122
132
|
msg["Subject"] = self.subject_template.format(metric_name=alert_data.metric_name)
|
|
123
133
|
|
|
124
|
-
# Attach
|
|
134
|
+
# Attach both parts. In multipart/alternative the LAST part is the
|
|
135
|
+
# preferred one, so HTML (with the brand logo) is shown when supported
|
|
136
|
+
# and the plain-text body remains the fallback.
|
|
125
137
|
msg.attach(MIMEText(message_body, "plain"))
|
|
138
|
+
msg.attach(MIMEText(self._build_html_body(message_body), "html"))
|
|
126
139
|
|
|
127
140
|
try:
|
|
128
141
|
# Connect to SMTP server
|
|
@@ -145,6 +158,37 @@ class EmailChannel(BaseAlertChannel):
|
|
|
145
158
|
|
|
146
159
|
return True
|
|
147
160
|
|
|
161
|
+
def _build_html_body(self, message_body: str) -> str:
|
|
162
|
+
"""Wrap the plain-text body in a branded HTML layout.
|
|
163
|
+
|
|
164
|
+
Renders a small header with the detectkit brand logo and name above the
|
|
165
|
+
message (kept in a ``<pre>`` so the alert-centric text layout carries
|
|
166
|
+
over verbatim). The logo is referenced by URL; clients that block remote
|
|
167
|
+
images simply fall back to the alt text and the plain-text part.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
message_body: The formatted plain-text alert body.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
An HTML document string.
|
|
174
|
+
"""
|
|
175
|
+
safe_body = html.escape(message_body)
|
|
176
|
+
return (
|
|
177
|
+
'<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,'
|
|
178
|
+
'Arial,sans-serif;color:#1b1916;max-width:640px">'
|
|
179
|
+
'<div style="margin-bottom:12px">'
|
|
180
|
+
f'<img src="{BRAND_ICON_URL}" width="28" height="28" '
|
|
181
|
+
f'alt="{html.escape(BRAND_USERNAME)}" '
|
|
182
|
+
'style="border-radius:6px;vertical-align:middle">'
|
|
183
|
+
'<span style="font-weight:600;font-size:16px;color:#d15b36;'
|
|
184
|
+
f'vertical-align:middle;margin-left:8px">{html.escape(BRAND_USERNAME)}</span>'
|
|
185
|
+
"</div>"
|
|
186
|
+
'<pre style="white-space:pre-wrap;font-family:JetBrains Mono,'
|
|
187
|
+
"ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;"
|
|
188
|
+
f'line-height:1.5;margin:0">{safe_body}</pre>'
|
|
189
|
+
"</div>"
|
|
190
|
+
)
|
|
191
|
+
|
|
148
192
|
def format_mentions(self, mentions: list[str]) -> str:
|
|
149
193
|
"""
|
|
150
194
|
Format mentions for email.
|
|
@@ -4,6 +4,7 @@ Mattermost alert channel.
|
|
|
4
4
|
Convenience wrapper around WebhookChannel for Mattermost.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from detectkit.alerting.channels.branding import BRAND_USERNAME
|
|
7
8
|
from detectkit.alerting.channels.webhook import WebhookChannel
|
|
8
9
|
|
|
9
10
|
|
|
@@ -15,10 +16,15 @@ class MattermostChannel(WebhookChannel):
|
|
|
15
16
|
for Mattermost. Mattermost webhooks are compatible with Slack API,
|
|
16
17
|
so WebhookChannel can be used directly.
|
|
17
18
|
|
|
19
|
+
The bot defaults to the **detectkit brand avatar** and name; override the
|
|
20
|
+
avatar with ``icon_url`` (a custom image) or opt out of the avatar with
|
|
21
|
+
``icon_emoji``. See :class:`WebhookChannel` for the icon precedence rules.
|
|
22
|
+
|
|
18
23
|
Parameters:
|
|
19
24
|
webhook_url (str): Mattermost incoming webhook URL
|
|
20
|
-
username (str): Bot username to display (default: "
|
|
21
|
-
|
|
25
|
+
username (str): Bot username to display (default: "detectkit")
|
|
26
|
+
icon_url (str): Bot avatar image URL (default: detectkit brand avatar)
|
|
27
|
+
icon_emoji (str): Bot emoji icon — use instead of an avatar image
|
|
22
28
|
timeout (int): Request timeout in seconds (default: 10)
|
|
23
29
|
|
|
24
30
|
Example:
|
|
@@ -31,8 +37,9 @@ class MattermostChannel(WebhookChannel):
|
|
|
31
37
|
def __init__(
|
|
32
38
|
self,
|
|
33
39
|
webhook_url: str,
|
|
34
|
-
username: str =
|
|
35
|
-
|
|
40
|
+
username: str = BRAND_USERNAME,
|
|
41
|
+
icon_url: str | None = None,
|
|
42
|
+
icon_emoji: str | None = None,
|
|
36
43
|
channel: str | None = None,
|
|
37
44
|
timeout: int = 10,
|
|
38
45
|
):
|
|
@@ -40,6 +47,7 @@ class MattermostChannel(WebhookChannel):
|
|
|
40
47
|
super().__init__(
|
|
41
48
|
webhook_url=webhook_url,
|
|
42
49
|
username=username,
|
|
50
|
+
icon_url=icon_url,
|
|
43
51
|
icon_emoji=icon_emoji,
|
|
44
52
|
channel=channel, # Optional: override webhook's default channel
|
|
45
53
|
timeout=timeout,
|
|
@@ -4,6 +4,7 @@ Slack alert channel.
|
|
|
4
4
|
Convenience wrapper around WebhookChannel for Slack.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from detectkit.alerting.channels.branding import BRAND_USERNAME
|
|
7
8
|
from detectkit.alerting.channels.webhook import WebhookChannel
|
|
8
9
|
|
|
9
10
|
|
|
@@ -14,10 +15,15 @@ class SlackChannel(WebhookChannel):
|
|
|
14
15
|
This is a convenience wrapper around WebhookChannel specifically
|
|
15
16
|
for Slack. Slack and Mattermost use compatible webhook formats.
|
|
16
17
|
|
|
18
|
+
The bot defaults to the **detectkit brand avatar** and name; override the
|
|
19
|
+
avatar with ``icon_url`` (a custom image) or opt out of the avatar with
|
|
20
|
+
``icon_emoji``. See :class:`WebhookChannel` for the icon precedence rules.
|
|
21
|
+
|
|
17
22
|
Parameters:
|
|
18
23
|
webhook_url (str): Slack incoming webhook URL
|
|
19
|
-
username (str): Bot username to display (default: "
|
|
20
|
-
|
|
24
|
+
username (str): Bot username to display (default: "detectkit")
|
|
25
|
+
icon_url (str): Bot avatar image URL (default: detectkit brand avatar)
|
|
26
|
+
icon_emoji (str): Bot emoji icon — use instead of an avatar image
|
|
21
27
|
channel (str): Target Slack channel (optional, e.g., "#alerts")
|
|
22
28
|
timeout (int): Request timeout in seconds (default: 10)
|
|
23
29
|
|
|
@@ -32,8 +38,9 @@ class SlackChannel(WebhookChannel):
|
|
|
32
38
|
def __init__(
|
|
33
39
|
self,
|
|
34
40
|
webhook_url: str,
|
|
35
|
-
username: str =
|
|
36
|
-
|
|
41
|
+
username: str = BRAND_USERNAME,
|
|
42
|
+
icon_url: str | None = None,
|
|
43
|
+
icon_emoji: str | None = None,
|
|
37
44
|
channel: str | None = None,
|
|
38
45
|
timeout: int = 10,
|
|
39
46
|
):
|
|
@@ -41,6 +48,7 @@ class SlackChannel(WebhookChannel):
|
|
|
41
48
|
super().__init__(
|
|
42
49
|
webhook_url=webhook_url,
|
|
43
50
|
username=username,
|
|
51
|
+
icon_url=icon_url,
|
|
44
52
|
icon_emoji=icon_emoji,
|
|
45
53
|
channel=channel,
|
|
46
54
|
timeout=timeout,
|
|
@@ -8,6 +8,7 @@ Compatible with Mattermost, Slack, and other webhook-based systems.
|
|
|
8
8
|
import requests
|
|
9
9
|
|
|
10
10
|
from detectkit.alerting.channels.base import AlertData, BaseAlertChannel
|
|
11
|
+
from detectkit.alerting.channels.branding import BRAND_ICON_URL, BRAND_USERNAME
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class WebhookChannel(BaseAlertChannel):
|
|
@@ -24,14 +25,21 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
24
25
|
{
|
|
25
26
|
"text": "message",
|
|
26
27
|
"username": "bot_name",
|
|
27
|
-
"icon_emoji": ":emoji:"
|
|
28
|
+
"icon_url": "https://.../bot-icon.png", # or "icon_emoji": ":emoji:"
|
|
28
29
|
"channel": "#channel" (optional)
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
Branding: the bot defaults to the **detectkit brand avatar** (``icon_url``)
|
|
33
|
+
and name (``username``). An explicit ``icon_url`` overrides it with a custom
|
|
34
|
+
image; an explicit ``icon_emoji`` opts out of the avatar in favor of an
|
|
35
|
+
emoji. Only one icon field is sent (``icon_url`` wins when both are set).
|
|
36
|
+
|
|
31
37
|
Parameters:
|
|
32
38
|
webhook_url (str): Webhook URL to send alerts to
|
|
33
|
-
username (str): Bot username to display (default: "
|
|
34
|
-
|
|
39
|
+
username (str): Bot username to display (default: "detectkit")
|
|
40
|
+
icon_url (str): Bot avatar image URL (default: detectkit brand avatar)
|
|
41
|
+
icon_emoji (str): Bot emoji icon — use instead of an avatar image
|
|
42
|
+
(default: None; falls back to the brand avatar)
|
|
35
43
|
channel (str): Target channel (optional, for Slack/Mattermost)
|
|
36
44
|
timeout (int): Request timeout in seconds (default: 10)
|
|
37
45
|
extra_headers (dict): Additional HTTP headers (optional)
|
|
@@ -58,8 +66,9 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
58
66
|
def __init__(
|
|
59
67
|
self,
|
|
60
68
|
webhook_url: str,
|
|
61
|
-
username: str =
|
|
62
|
-
|
|
69
|
+
username: str = BRAND_USERNAME,
|
|
70
|
+
icon_url: str | None = None,
|
|
71
|
+
icon_emoji: str | None = None,
|
|
63
72
|
channel: str | None = None,
|
|
64
73
|
timeout: int = 10,
|
|
65
74
|
extra_headers: dict[str, str] | None = None,
|
|
@@ -70,6 +79,12 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
70
79
|
|
|
71
80
|
self.webhook_url = webhook_url
|
|
72
81
|
self.username = username
|
|
82
|
+
# Default to the detectkit brand avatar. An explicit icon_url or
|
|
83
|
+
# icon_emoji opts out of the default; we only fill in the brand avatar
|
|
84
|
+
# when the user configured neither.
|
|
85
|
+
if icon_url is None and icon_emoji is None:
|
|
86
|
+
icon_url = BRAND_ICON_URL
|
|
87
|
+
self.icon_url = icon_url
|
|
73
88
|
self.icon_emoji = icon_emoji
|
|
74
89
|
self.channel = channel
|
|
75
90
|
self.timeout = timeout
|
|
@@ -118,12 +133,18 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
118
133
|
"text": body,
|
|
119
134
|
}
|
|
120
135
|
|
|
121
|
-
payload = {
|
|
136
|
+
payload: dict[str, object] = {
|
|
122
137
|
"username": self.username,
|
|
123
|
-
"icon_emoji": self.icon_emoji,
|
|
124
138
|
"attachments": [attachment],
|
|
125
139
|
}
|
|
126
140
|
|
|
141
|
+
# Send exactly one icon field: the avatar image (brand default or a
|
|
142
|
+
# custom override) takes precedence over an emoji.
|
|
143
|
+
if self.icon_url:
|
|
144
|
+
payload["icon_url"] = self.icon_url
|
|
145
|
+
elif self.icon_emoji:
|
|
146
|
+
payload["icon_emoji"] = self.icon_emoji
|
|
147
|
+
|
|
127
148
|
# Add channel if specified (for Slack)
|
|
128
149
|
if self.channel:
|
|
129
150
|
payload["channel"] = self.channel
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
detectkit is a Python library and CLI (`dtk`) for monitoring time-series
|
|
4
4
|
metrics with automatic anomaly detection and multi-channel alerting. It is
|
|
5
5
|
**dbt-like**: metrics live as YAML + SQL in a project directory, and you run
|
|
6
|
-
them with one command. Core logic is pure numpy (no pandas). **ClickHouse
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
them with one command. Core logic is pure numpy (no pandas). **ClickHouse,
|
|
7
|
+
PostgreSQL and MySQL are all fully supported** — only the connection and the SQL
|
|
8
|
+
dialect of your metric queries differ between them.
|
|
9
9
|
|
|
10
10
|
## The pipeline: load → detect → alert
|
|
11
11
|
|
|
@@ -78,11 +78,11 @@ alert_channels:
|
|
|
78
78
|
|
|
79
79
|
### Database profiles
|
|
80
80
|
|
|
81
|
-
> ClickHouse
|
|
82
|
-
>
|
|
83
|
-
> `
|
|
81
|
+
> ClickHouse, PostgreSQL and MySQL are all fully supported. ClickHouse/MySQL use
|
|
82
|
+
> two *databases*; PostgreSQL connects to one `database` and uses two *schemas*.
|
|
83
|
+
> `dtk init --db-type {clickhouse,postgres,mysql}` scaffolds the right shape.
|
|
84
84
|
|
|
85
|
-
**ClickHouse
|
|
85
|
+
**ClickHouse**:
|
|
86
86
|
```yaml
|
|
87
87
|
profiles:
|
|
88
88
|
prod:
|
|
@@ -98,36 +98,34 @@ profiles:
|
|
|
98
98
|
max_memory_usage: 10000000000
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
**PostgreSQL** (
|
|
101
|
+
**PostgreSQL** (connect to `database`, tables in schemas):
|
|
102
102
|
```yaml
|
|
103
103
|
profiles:
|
|
104
104
|
prod:
|
|
105
105
|
type: postgres
|
|
106
106
|
host: localhost
|
|
107
107
|
port: 5432
|
|
108
|
-
database:
|
|
108
|
+
database: detectkit # required — must already exist
|
|
109
109
|
user: postgres
|
|
110
110
|
password: "..."
|
|
111
|
-
internal_schema: detectkit # required — _dtk_* tables
|
|
111
|
+
internal_schema: detectkit # required — _dtk_* tables (auto-created)
|
|
112
112
|
data_schema: public # required — data queries
|
|
113
|
-
|
|
114
|
-
max_overflow: 10 # optional
|
|
113
|
+
settings: {} # optional — extra psycopg2.connect kwargs
|
|
115
114
|
```
|
|
116
115
|
|
|
117
|
-
**MySQL** (
|
|
116
|
+
**MySQL** (8.0+; two databases):
|
|
118
117
|
```yaml
|
|
119
118
|
profiles:
|
|
120
119
|
prod:
|
|
121
120
|
type: mysql
|
|
122
121
|
host: localhost
|
|
123
122
|
port: 3306
|
|
124
|
-
database: analytics # required
|
|
125
123
|
user: root
|
|
126
124
|
password: "..."
|
|
127
|
-
internal_database: detectkit # required
|
|
125
|
+
internal_database: detectkit # required — _dtk_* tables (auto-created)
|
|
128
126
|
data_database: analytics # required
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
database: analytics # optional — default db for the connection
|
|
128
|
+
settings: {} # optional — extra pymysql.connect kwargs
|
|
131
129
|
```
|
|
132
130
|
|
|
133
131
|
### Alert channels
|
|
@@ -135,17 +133,24 @@ profiles:
|
|
|
135
133
|
Defined once in `profiles.yml`, referenced by name in each metric's
|
|
136
134
|
`alerting.channels` (and in `error_alerting.channels`).
|
|
137
135
|
|
|
136
|
+
The bot defaults to the **detectkit brand** name + avatar on every channel.
|
|
137
|
+
Override per channel; Telegram and email brand differently (see their notes).
|
|
138
|
+
|
|
138
139
|
**Mattermost** / **Slack** (Slack-compatible webhook API, same fields):
|
|
139
140
|
```yaml
|
|
140
141
|
alert_channels:
|
|
141
142
|
mattermost_ops:
|
|
142
143
|
type: mattermost # or: slack
|
|
143
144
|
webhook_url: "{{ env_var('MATTERMOST_WEBHOOK_URL') }}" # required
|
|
144
|
-
username: "detectkit" # optional (default: "detectkit")
|
|
145
|
-
icon_emoji: ":warning:" # optional
|
|
146
145
|
channel: "alerts" # optional — override webhook default ("#alerts" for Slack)
|
|
147
146
|
timeout: 10 # optional HTTP timeout (s)
|
|
147
|
+
# Bot identity defaults to the detectkit brand; override any of:
|
|
148
|
+
# username: "detectkit" # display name
|
|
149
|
+
# icon_url: "https://.../bot.png" # avatar image (default: brand avatar)
|
|
150
|
+
# icon_emoji: ":warning:" # emoji instead of an avatar image
|
|
148
151
|
```
|
|
152
|
+
> Icon precedence: `icon_url` (default: brand avatar) wins over `icon_emoji`;
|
|
153
|
+
> set either to opt out of the brand avatar.
|
|
149
154
|
> Slack note: `@username` in a Slack webhook is **display-only** (no real ping).
|
|
150
155
|
> Use Slack user IDs (`U…`) for real pings.
|
|
151
156
|
|
|
@@ -157,6 +162,9 @@ alert_channels:
|
|
|
157
162
|
bot_token: "{{ env_var('TG_BOT_TOKEN') }}" # required
|
|
158
163
|
chat_id: "-1001234567890" # required
|
|
159
164
|
```
|
|
165
|
+
> Telegram shows the bot account's own avatar (set in @BotFather, not
|
|
166
|
+
> per-message), so detectkit can't override it. Brand it via `/setuserpic` —
|
|
167
|
+
> the detectkit avatar is at `https://dtk.pipelab.dev/bot-icon.png`.
|
|
160
168
|
|
|
161
169
|
**Email** (SMTP):
|
|
162
170
|
```yaml
|
|
@@ -167,10 +175,14 @@ alert_channels:
|
|
|
167
175
|
smtp_port: 587 # required (587 TLS, 465 SSL)
|
|
168
176
|
from_email: "alerts@example.com" # required
|
|
169
177
|
to_emails: ["ops@example.com"] # required (list)
|
|
178
|
+
from_name: "detectkit" # optional — From display name (default: "detectkit")
|
|
170
179
|
smtp_username: "..." # optional
|
|
171
180
|
smtp_password: "..." # optional (use env_var)
|
|
172
181
|
use_tls: true # optional (default: true)
|
|
173
182
|
```
|
|
183
|
+
> Sends as `detectkit <from_email>` with the brand logo in an HTML body (plain
|
|
184
|
+
> text stays the fallback). The avatar mail clients show is set by the sending
|
|
185
|
+
> domain (BIMI), not the message — brand it via `from_name` + your domain.
|
|
174
186
|
|
|
175
187
|
**Webhook** (generic):
|
|
176
188
|
```yaml
|
|
@@ -185,12 +197,12 @@ alert_channels:
|
|
|
185
197
|
## Notes
|
|
186
198
|
|
|
187
199
|
- **First-run setup:** the `profiles.yml` that `dtk init` writes is a
|
|
188
|
-
placeholder
|
|
189
|
-
at example values
|
|
190
|
-
credentials and
|
|
191
|
-
(the **`dtk-setup-project`** skill walks this).
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
placeholder scaffolded for `--db-type` (default ClickHouse) — its `dev`
|
|
201
|
+
profile points the location fields at example values on `localhost`. Edit the
|
|
202
|
+
host, credentials and location names to match your environment before running
|
|
203
|
+
(the **`dtk-setup-project`** skill walks this). ClickHouse/MySQL use
|
|
204
|
+
`internal_database` / `data_database` (no `database:` field on ClickHouse);
|
|
205
|
+
PostgreSQL connects to a `database` and uses `internal_schema` / `data_schema`.
|
|
194
206
|
- `dtk run` (without `--profile`) uses the `default_profile` declared in
|
|
195
207
|
**`profiles.yml`**; the `default_profile` in `detectkit_project.yml` is not
|
|
196
208
|
read at runtime — keep them in sync to avoid confusion.
|
{detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md
RENAMED
|
@@ -40,12 +40,17 @@ side by side, ask which one to set up.
|
|
|
40
40
|
|
|
41
41
|
## Step 1 — Pick the database backend
|
|
42
42
|
|
|
43
|
-
**ClickHouse
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
**ClickHouse, PostgreSQL and MySQL are all fully supported.** Ask which one the
|
|
44
|
+
project uses (default to ClickHouse if unsure). `dtk init --db-type
|
|
45
|
+
{clickhouse,postgres,mysql}` scaffolds `profiles.yml` for the chosen backend.
|
|
46
|
+
The location fields differ:
|
|
47
|
+
- **ClickHouse** / **MySQL** — two *databases*: `internal_database` / `data_database`.
|
|
48
|
+
- **PostgreSQL** — connect to a `database` (must already exist), then two
|
|
49
|
+
*schemas*: `internal_schema` / `data_schema`.
|
|
50
|
+
|
|
51
|
+
The metric query SQL dialect also differs (e.g. `toStartOfInterval` on
|
|
52
|
+
ClickHouse vs `date_trunc`/`to_timestamp` on Postgres vs `FROM_UNIXTIME` on
|
|
53
|
+
MySQL). Everything else — detectors, alerting, the CLI — is identical.
|
|
49
54
|
|
|
50
55
|
## Step 2 — Connection details (gather, don't guess)
|
|
51
56
|
|
|
@@ -74,8 +79,8 @@ values):
|
|
|
74
79
|
data, e.g. `detectkit` or `monitoring`.
|
|
75
80
|
- `data_database` — where the source tables your metric queries read from live.
|
|
76
81
|
|
|
77
|
-
For Postgres these are `internal_schema` / `data_schema
|
|
78
|
-
`
|
|
82
|
+
For Postgres these are `internal_schema` / `data_schema` (inside the connected
|
|
83
|
+
`database`); for MySQL they are `internal_database` / `data_database`.
|
|
79
84
|
|
|
80
85
|
## Step 4 — Profile name & `default_profile`
|
|
81
86
|
|
|
@@ -111,14 +116,19 @@ If the user wants alerts now, add one channel under `alert_channels:` (it is
|
|
|
111
116
|
referenced by name from each metric's `alerting.channels`). Pick the type and
|
|
112
117
|
gather its required fields (see `project.md` for the full set per type):
|
|
113
118
|
|
|
119
|
+
The bot defaults to the **detectkit brand** name + avatar everywhere; the
|
|
120
|
+
identity fields below are optional overrides.
|
|
121
|
+
|
|
114
122
|
- **mattermost** / **slack** — `webhook_url` (use `env_var`); optional
|
|
115
|
-
`channel`, `username`, `icon_emoji`.
|
|
116
|
-
- **telegram** — `bot_token` (env_var) + `chat_id`.
|
|
123
|
+
`channel`, `username`, `icon_url` (avatar; default brand), `icon_emoji`.
|
|
124
|
+
- **telegram** — `bot_token` (env_var) + `chat_id`. (Bot avatar is set in
|
|
125
|
+
@BotFather, not by detectkit.)
|
|
117
126
|
- **email** — `smtp_host`, `smtp_port`, `from_email`, `to_emails` (list);
|
|
127
|
+
optional `from_name` (From display name, default `detectkit`);
|
|
118
128
|
`smtp_username` / `smtp_password` via env_var.
|
|
119
129
|
- **webhook** — generic `webhook_url` (required) + optional `extra_headers`
|
|
120
|
-
(also accepts `username` / `icon_emoji` / `channel`). There is
|
|
121
|
-
`method` or `headers` field.
|
|
130
|
+
(also accepts `username` / `icon_url` / `icon_emoji` / `channel`). There is
|
|
131
|
+
no `url`, `method` or `headers` field.
|
|
122
132
|
|
|
123
133
|
```yaml
|
|
124
134
|
# at the end of profiles.yml
|