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.
Files changed (104) hide show
  1. {detectkit-0.11.0/detectkit.egg-info → detectkit-0.12.0}/PKG-INFO +1 -1
  2. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/__init__.py +1 -1
  3. detectkit-0.12.0/detectkit/alerting/channels/branding.py +20 -0
  4. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/email.py +46 -2
  5. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/mattermost.py +12 -4
  6. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/slack.py +12 -4
  7. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/webhook.py +28 -7
  8. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/project.md +16 -2
  9. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +9 -4
  10. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/init.py +7 -5
  11. {detectkit-0.11.0 → detectkit-0.12.0/detectkit.egg-info}/PKG-INFO +1 -1
  12. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit.egg-info/SOURCES.txt +1 -0
  13. {detectkit-0.11.0 → detectkit-0.12.0}/LICENSE +0 -0
  14. {detectkit-0.11.0 → detectkit-0.12.0}/MANIFEST.in +0 -0
  15. {detectkit-0.11.0 → detectkit-0.12.0}/README.md +0 -0
  16. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/__init__.py +0 -0
  17. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/__init__.py +0 -0
  18. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/base.py +0 -0
  19. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/factory.py +0 -0
  20. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/channels/telegram.py +0 -0
  21. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  22. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  23. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  24. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  25. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  26. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  27. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  28. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  29. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/__init__.py +0 -0
  30. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/_output.py +0 -0
  31. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  32. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  33. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  34. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  35. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  36. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  37. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  38. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/__init__.py +0 -0
  39. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/clean.py +0 -0
  40. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/init_claude.py +0 -0
  41. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/run.py +0 -0
  42. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/test_alert.py +0 -0
  43. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/commands/unlock.py +0 -0
  44. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/cli/main.py +0 -0
  45. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/config/__init__.py +0 -0
  46. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/config/metric_config.py +0 -0
  47. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/config/profile.py +0 -0
  48. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/config/project_config.py +0 -0
  49. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/config/validator.py +0 -0
  50. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/core/__init__.py +0 -0
  51. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/core/interval.py +0 -0
  52. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/core/models.py +0 -0
  53. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/__init__.py +0 -0
  54. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/_sql_manager.py +0 -0
  55. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/clickhouse_manager.py +0 -0
  56. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/__init__.py +0 -0
  57. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  58. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_base.py +0 -0
  59. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  60. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_detections.py +0 -0
  61. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  62. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  63. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_schema.py +0 -0
  64. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  65. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/internal_tables/manager.py +0 -0
  66. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/manager.py +0 -0
  67. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/mysql_manager.py +0 -0
  68. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/postgres_manager.py +0 -0
  69. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/database/tables.py +0 -0
  70. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/__init__.py +0 -0
  71. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/base.py +0 -0
  72. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/factory.py +0 -0
  73. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/seasonality.py +0 -0
  74. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/statistical/__init__.py +0 -0
  75. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  76. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/statistical/iqr.py +0 -0
  77. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/statistical/mad.py +0 -0
  78. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  79. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/detectors/statistical/zscore.py +0 -0
  80. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/loaders/__init__.py +0 -0
  81. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/loaders/metric_loader.py +0 -0
  82. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/loaders/query_template.py +0 -0
  83. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/__init__.py +0 -0
  84. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/error_dispatch.py +0 -0
  85. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  86. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  87. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  88. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  89. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  90. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  91. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  92. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/utils/__init__.py +0 -0
  93. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/utils/datetime_utils.py +0 -0
  94. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/utils/env_interpolation.py +0 -0
  95. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/utils/json_utils.py +0 -0
  96. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit/utils/stats.py +0 -0
  97. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit.egg-info/dependency_links.txt +0 -0
  98. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit.egg-info/entry_points.txt +0 -0
  99. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit.egg-info/requires.txt +0 -0
  100. {detectkit-0.11.0 → detectkit-0.12.0}/detectkit.egg-info/top_level.txt +0 -0
  101. {detectkit-0.11.0 → detectkit-0.12.0}/pyproject.toml +0 -0
  102. {detectkit-0.11.0 → detectkit-0.12.0}/requirements.txt +0 -0
  103. {detectkit-0.11.0 → detectkit-0.12.0}/setup.cfg +0 -0
  104. {detectkit-0.11.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.11.0
3
+ Version: 0.12.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -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.11.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
- msg["From"] = self.from_email
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 plain text body
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: "detectk")
21
- icon_emoji (str): Bot emoji icon (default: ":warning:")
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 = "detectk",
35
- icon_emoji: str = ":warning:",
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: "detectk")
20
- icon_emoji (str): Bot emoji icon (default: ":warning:")
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 = "detectk",
36
- icon_emoji: str = ":warning:",
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: "detectk")
34
- icon_emoji (str): Bot emoji icon (default: ":warning:")
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 = "detectk",
62
- icon_emoji: str = ":warning:",
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
@@ -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 no `url`,
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, icon_emoji, channel,
138
- # timeout. NOTE: there is no `icon_url` param use `icon_emoji`.
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
- icon_emoji: ":warning:"
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') }}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.11.0
3
+ Version: 0.12.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -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