detectkit 0.11.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.11.0/detectkit.egg-info → detectkit-0.12.0}/PKG-INFO +1 -1
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/__init__.py +1 -1
- detectkit-0.12.0/detectkit/alerting/channels/branding.py +20 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/email.py +46 -2
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/mattermost.py +12 -4
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/slack.py +12 -4
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/webhook.py +28 -7
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/project.md +16 -2
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +9 -4
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/init.py +7 -5
- {detectkit-0.11.0 → detectkit-0.12.0/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit.egg-info/SOURCES.txt +1 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/LICENSE +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/MANIFEST.in +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/README.md +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/core/models.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/pyproject.toml +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/requirements.txt +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/setup.cfg +0 -0
- {detectkit-0.11.0 → detectkit-0.12.0}/setup.py +0 -0
|
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
|
|
|
4
4
|
A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.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
|
|
@@ -133,17 +133,24 @@ profiles:
|
|
|
133
133
|
Defined once in `profiles.yml`, referenced by name in each metric's
|
|
134
134
|
`alerting.channels` (and in `error_alerting.channels`).
|
|
135
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
|
+
|
|
136
139
|
**Mattermost** / **Slack** (Slack-compatible webhook API, same fields):
|
|
137
140
|
```yaml
|
|
138
141
|
alert_channels:
|
|
139
142
|
mattermost_ops:
|
|
140
143
|
type: mattermost # or: slack
|
|
141
144
|
webhook_url: "{{ env_var('MATTERMOST_WEBHOOK_URL') }}" # required
|
|
142
|
-
username: "detectkit" # optional (default: "detectkit")
|
|
143
|
-
icon_emoji: ":warning:" # optional
|
|
144
145
|
channel: "alerts" # optional — override webhook default ("#alerts" for Slack)
|
|
145
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
|
|
146
151
|
```
|
|
152
|
+
> Icon precedence: `icon_url` (default: brand avatar) wins over `icon_emoji`;
|
|
153
|
+
> set either to opt out of the brand avatar.
|
|
147
154
|
> Slack note: `@username` in a Slack webhook is **display-only** (no real ping).
|
|
148
155
|
> Use Slack user IDs (`U…`) for real pings.
|
|
149
156
|
|
|
@@ -155,6 +162,9 @@ alert_channels:
|
|
|
155
162
|
bot_token: "{{ env_var('TG_BOT_TOKEN') }}" # required
|
|
156
163
|
chat_id: "-1001234567890" # required
|
|
157
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`.
|
|
158
168
|
|
|
159
169
|
**Email** (SMTP):
|
|
160
170
|
```yaml
|
|
@@ -165,10 +175,14 @@ alert_channels:
|
|
|
165
175
|
smtp_port: 587 # required (587 TLS, 465 SSL)
|
|
166
176
|
from_email: "alerts@example.com" # required
|
|
167
177
|
to_emails: ["ops@example.com"] # required (list)
|
|
178
|
+
from_name: "detectkit" # optional — From display name (default: "detectkit")
|
|
168
179
|
smtp_username: "..." # optional
|
|
169
180
|
smtp_password: "..." # optional (use env_var)
|
|
170
181
|
use_tls: true # optional (default: true)
|
|
171
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.
|
|
172
186
|
|
|
173
187
|
**Webhook** (generic):
|
|
174
188
|
```yaml
|
{detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md
RENAMED
|
@@ -116,14 +116,19 @@ If the user wants alerts now, add one channel under `alert_channels:` (it is
|
|
|
116
116
|
referenced by name from each metric's `alerting.channels`). Pick the type and
|
|
117
117
|
gather its required fields (see `project.md` for the full set per type):
|
|
118
118
|
|
|
119
|
+
The bot defaults to the **detectkit brand** name + avatar everywhere; the
|
|
120
|
+
identity fields below are optional overrides.
|
|
121
|
+
|
|
119
122
|
- **mattermost** / **slack** — `webhook_url` (use `env_var`); optional
|
|
120
|
-
`channel`, `username`, `icon_emoji`.
|
|
121
|
-
- **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.)
|
|
122
126
|
- **email** — `smtp_host`, `smtp_port`, `from_email`, `to_emails` (list);
|
|
127
|
+
optional `from_name` (From display name, default `detectkit`);
|
|
123
128
|
`smtp_username` / `smtp_password` via env_var.
|
|
124
129
|
- **webhook** — generic `webhook_url` (required) + optional `extra_headers`
|
|
125
|
-
(also accepts `username` / `icon_emoji` / `channel`). There is
|
|
126
|
-
`method` or `headers` field.
|
|
130
|
+
(also accepts `username` / `icon_url` / `icon_emoji` / `channel`). There is
|
|
131
|
+
no `url`, `method` or `headers` field.
|
|
127
132
|
|
|
128
133
|
```yaml
|
|
129
134
|
# at the end of profiles.yml
|
|
@@ -134,20 +134,21 @@ _BUCKET_SQL = {
|
|
|
134
134
|
_ALERT_CHANNELS = """
|
|
135
135
|
# Alert channels (referenced by name from a metric's alerting.channels)
|
|
136
136
|
alert_channels:
|
|
137
|
-
# Mattermost. Supported keys: webhook_url, username,
|
|
138
|
-
# timeout.
|
|
137
|
+
# Mattermost. Supported keys: webhook_url, username, icon_url, icon_emoji,
|
|
138
|
+
# channel, timeout. The bot name + avatar default to the detectkit brand;
|
|
139
|
+
# override the avatar with icon_url (an image URL) or icon_emoji (an emoji).
|
|
139
140
|
mattermost_alerts:
|
|
140
141
|
type: mattermost
|
|
141
142
|
webhook_url: "{{ env_var('MATTERMOST_WEBHOOK_URL') }}"
|
|
142
|
-
username: detectkit
|
|
143
|
-
|
|
143
|
+
# username: detectkit # optional — override the display name
|
|
144
|
+
# icon_url: "https://.../bot.png" # optional — override the brand avatar
|
|
145
|
+
# icon_emoji: ":warning:" # optional — emoji instead of an avatar
|
|
144
146
|
|
|
145
147
|
# Slack example (same fields as mattermost)
|
|
146
148
|
# slack_alerts:
|
|
147
149
|
# type: slack
|
|
148
150
|
# webhook_url: "{{ env_var('SLACK_WEBHOOK_URL') }}"
|
|
149
151
|
# channel: "#alerts"
|
|
150
|
-
# username: detectkit
|
|
151
152
|
|
|
152
153
|
# Telegram example (required: bot_token, chat_id)
|
|
153
154
|
# telegram_alerts:
|
|
@@ -161,6 +162,7 @@ alert_channels:
|
|
|
161
162
|
# smtp_host: smtp.gmail.com
|
|
162
163
|
# smtp_port: 587
|
|
163
164
|
# from_email: alerts@example.com
|
|
165
|
+
# from_name: detectkit # optional — From display name (default: detectkit)
|
|
164
166
|
# to_emails:
|
|
165
167
|
# - team@example.com
|
|
166
168
|
# smtp_username: "{{ env_var('SMTP_USERNAME') }}"
|
|
@@ -14,6 +14,7 @@ detectkit.egg-info/top_level.txt
|
|
|
14
14
|
detectkit/alerting/__init__.py
|
|
15
15
|
detectkit/alerting/channels/__init__.py
|
|
16
16
|
detectkit/alerting/channels/base.py
|
|
17
|
+
detectkit/alerting/channels/branding.py
|
|
17
18
|
detectkit/alerting/channels/email.py
|
|
18
19
|
detectkit/alerting/channels/factory.py
|
|
19
20
|
detectkit/alerting/channels/mattermost.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|