detectkit 0.14.0__tar.gz → 0.15.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.14.0/detectkit.egg-info → detectkit-0.15.0}/PKG-INFO +1 -1
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/__init__.py +1 -1
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/base.py +15 -11
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/email.py +31 -5
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/telegram.py +6 -1
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/webhook.py +7 -2
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_base.py +6 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_decision.py +2 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_recovery.py +1 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/alerting.md +23 -1
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/project.md +7 -3
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_alert_step.py +1 -0
- {detectkit-0.14.0 → detectkit-0.15.0/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.14.0 → detectkit-0.15.0}/LICENSE +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/MANIFEST.in +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/README.md +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/core/models.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/pyproject.toml +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/requirements.txt +0 -0
- {detectkit-0.14.0 → detectkit-0.15.0}/setup.cfg +0 -0
- {detectkit-0.14.0 → detectkit-0.15.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.15.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -32,10 +32,14 @@ class AlertData:
|
|
|
32
32
|
consecutive_count: Number of consecutive anomalies
|
|
33
33
|
is_recovery: True for recovery notifications
|
|
34
34
|
is_no_data: True for missing-data alerts (no_data_alert)
|
|
35
|
-
project_name: Optional ``detectkit_project.yml`` name. Surfaces
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
project_name: Optional ``detectkit_project.yml`` name. Surfaces as
|
|
36
|
+
``{project_name}`` / ``{project_name_prefix}`` in templates and, by
|
|
37
|
+
default, as a ``[name] `` prefix on every alert title/headline
|
|
38
|
+
(anomaly, recovery, no-data, error) plus a brand-paired footer
|
|
39
|
+
("detectkit · name" on webhook/email). The detectkit pipeline stamps
|
|
40
|
+
it from the project config; direct-API callers leave it ``None`` and
|
|
41
|
+
render unchanged. Lets multiple projects share one alert channel —
|
|
42
|
+
keeping the default brand bot name + avatar — without ambiguity.
|
|
39
43
|
|
|
40
44
|
Alert-rule fields (``min_detectors``, ``direction_policy``,
|
|
41
45
|
``consecutive_required``, ``detector_count``) describe *why the alert
|
|
@@ -450,7 +454,7 @@ class BaseAlertChannel(ABC):
|
|
|
450
454
|
Default template string
|
|
451
455
|
"""
|
|
452
456
|
return (
|
|
453
|
-
"🔴 Alert: {metric_name}\n"
|
|
457
|
+
"🔴 {project_name_prefix}Alert: {metric_name}\n"
|
|
454
458
|
"{description_line}"
|
|
455
459
|
"Quorum {detector_count}/{min_detectors} · "
|
|
456
460
|
"direction {direction} (policy {direction_policy}) · "
|
|
@@ -476,7 +480,7 @@ class BaseAlertChannel(ABC):
|
|
|
476
480
|
Default recovery template string
|
|
477
481
|
"""
|
|
478
482
|
return (
|
|
479
|
-
"🟢 Alert cleared: {metric_name}\n"
|
|
483
|
+
"🟢 {project_name_prefix}Alert cleared: {metric_name}\n"
|
|
480
484
|
"{description_line}"
|
|
481
485
|
"The alert condition no longer holds — "
|
|
482
486
|
"the metric is back within expected bounds.\n"
|
|
@@ -500,7 +504,7 @@ class BaseAlertChannel(ABC):
|
|
|
500
504
|
Returns:
|
|
501
505
|
Default title template string
|
|
502
506
|
"""
|
|
503
|
-
return "🔴 Alert: {metric_name}"
|
|
507
|
+
return "🔴 {project_name_prefix}Alert: {metric_name}"
|
|
504
508
|
|
|
505
509
|
def get_default_recovery_title_template(self) -> str:
|
|
506
510
|
"""
|
|
@@ -509,7 +513,7 @@ class BaseAlertChannel(ABC):
|
|
|
509
513
|
Returns:
|
|
510
514
|
Default recovery title template string
|
|
511
515
|
"""
|
|
512
|
-
return "🟢 Alert cleared: {metric_name}"
|
|
516
|
+
return "🟢 {project_name_prefix}Alert cleared: {metric_name}"
|
|
513
517
|
|
|
514
518
|
def get_default_no_data_template(self) -> str:
|
|
515
519
|
"""
|
|
@@ -519,7 +523,7 @@ class BaseAlertChannel(ABC):
|
|
|
519
523
|
has no datapoint (no row OR row with NULL/NaN value).
|
|
520
524
|
"""
|
|
521
525
|
return (
|
|
522
|
-
"🟡 No data for metric: {metric_name}\n"
|
|
526
|
+
"🟡 {project_name_prefix}No data for metric: {metric_name}\n"
|
|
523
527
|
"{description_line}"
|
|
524
528
|
"Time: {timestamp}\n"
|
|
525
529
|
"Status: query returned no datapoint for the latest interval\n"
|
|
@@ -529,12 +533,12 @@ class BaseAlertChannel(ABC):
|
|
|
529
533
|
|
|
530
534
|
def get_default_no_data_title_template(self) -> str:
|
|
531
535
|
"""Get default title template for no-data alerts."""
|
|
532
|
-
return "🟡 No data: {metric_name}"
|
|
536
|
+
return "🟡 {project_name_prefix}No data: {metric_name}"
|
|
533
537
|
|
|
534
538
|
def get_default_error_template(self) -> str:
|
|
535
539
|
"""Default body template for project-level error alerts."""
|
|
536
540
|
return (
|
|
537
|
-
"🔵 Pipeline failed for metric: {metric_name}\n"
|
|
541
|
+
"🔵 {project_name_prefix}Pipeline failed for metric: {metric_name}\n"
|
|
538
542
|
"{description_line}"
|
|
539
543
|
"Time: {timestamp}\n"
|
|
540
544
|
"Error: {error_type}: {error_message}\n"
|
|
@@ -74,7 +74,7 @@ class EmailChannel(BaseAlertChannel):
|
|
|
74
74
|
smtp_username: str | None = None,
|
|
75
75
|
smtp_password: str | None = None,
|
|
76
76
|
use_tls: bool = True,
|
|
77
|
-
subject_template: str = "🔴 Alert: {metric_name}",
|
|
77
|
+
subject_template: str = "🔴 {project_name_prefix}Alert: {metric_name}",
|
|
78
78
|
from_name: str = BRAND_USERNAME,
|
|
79
79
|
template: str | None = None,
|
|
80
80
|
**kwargs,
|
|
@@ -144,10 +144,18 @@ class EmailChannel(BaseAlertChannel):
|
|
|
144
144
|
# of a bot display name. formataddr quotes the name when required.
|
|
145
145
|
msg["From"] = formataddr((self.from_name, self.from_email))
|
|
146
146
|
msg["To"] = ", ".join(self.to_emails)
|
|
147
|
-
# Strip CR/LF from the metric name before
|
|
148
|
-
#
|
|
147
|
+
# Strip CR/LF from the metric *and* project name before they reach the
|
|
148
|
+
# Subject header so neither can inject extra email headers. The project
|
|
149
|
+
# prefix ("[my_project] ") makes the inbox row distinguishable when
|
|
150
|
+
# several projects email the same address.
|
|
149
151
|
subject_metric = alert_data.metric_name.replace("\r", " ").replace("\n", " ")
|
|
150
|
-
|
|
152
|
+
project_clean = (alert_data.project_name or "").replace("\r", " ").replace("\n", " ")
|
|
153
|
+
project_prefix = f"[{project_clean}] " if project_clean else ""
|
|
154
|
+
msg["Subject"] = self.subject_template.format(
|
|
155
|
+
metric_name=subject_metric,
|
|
156
|
+
project_name_prefix=project_prefix,
|
|
157
|
+
project_name=project_clean,
|
|
158
|
+
)
|
|
151
159
|
|
|
152
160
|
# Attach both parts. In multipart/alternative the LAST part is the
|
|
153
161
|
# preferred one, so HTML (the branded card) is shown when supported and
|
|
@@ -247,6 +255,8 @@ class EmailChannel(BaseAlertChannel):
|
|
|
247
255
|
f'font-size:4px;background-color:{accent};"> </td></tr>'
|
|
248
256
|
# header: logo + wordmark + status pill
|
|
249
257
|
f"{self._header_html(accent, pill)}"
|
|
258
|
+
# project eyebrow (small label above the metric, only when set)
|
|
259
|
+
f"{self._eyebrow_html(ctx['project_name'])}"
|
|
250
260
|
# title
|
|
251
261
|
f'<tr><td style="padding:6px 24px 2px 24px;font-family:{_SANS};font-size:22px;'
|
|
252
262
|
f'font-weight:bold;color:{_INK};mso-line-height-rule:exactly;line-height:28px;">'
|
|
@@ -258,6 +268,17 @@ class EmailChannel(BaseAlertChannel):
|
|
|
258
268
|
"</table></td></tr></table></body></html>"
|
|
259
269
|
)
|
|
260
270
|
|
|
271
|
+
def _eyebrow_html(self, project_name: str) -> str:
|
|
272
|
+
"""Small uppercase project label above the metric title (empty if unset)."""
|
|
273
|
+
if not project_name:
|
|
274
|
+
return ""
|
|
275
|
+
return (
|
|
276
|
+
f'<tr><td style="padding:10px 24px 0 24px;font-family:{_SANS};font-size:11px;'
|
|
277
|
+
f"font-weight:bold;letter-spacing:0.5px;color:{_FAINT};text-transform:uppercase;"
|
|
278
|
+
f'mso-line-height-rule:exactly;line-height:14px;">'
|
|
279
|
+
f"{html.escape(project_name)}</td></tr>"
|
|
280
|
+
)
|
|
281
|
+
|
|
261
282
|
def _header_html(self, accent: str, pill: str) -> str:
|
|
262
283
|
"""Logo + wordmark (real text) + colored status pill row."""
|
|
263
284
|
return (
|
|
@@ -432,11 +453,16 @@ class EmailChannel(BaseAlertChannel):
|
|
|
432
453
|
def _footer_html(self, alert_data: AlertData) -> str:
|
|
433
454
|
cc = self.format_mentions(alert_data.mentions)
|
|
434
455
|
cc_html = f" · {html.escape(cc)}" if cc else ""
|
|
456
|
+
# Pair the brand with the project name ("Sent by detectkit · my_project")
|
|
457
|
+
# so the source project is clear even past the subject/eyebrow.
|
|
458
|
+
project_html = (
|
|
459
|
+
f" · {html.escape(alert_data.project_name)}" if alert_data.project_name else ""
|
|
460
|
+
)
|
|
435
461
|
return (
|
|
436
462
|
f'<tr><td bgcolor="{_SURFACE}" style="background-color:{_SURFACE};'
|
|
437
463
|
f"border-top:1px solid {_BORDER};padding:14px 24px;font-family:{_SANS};"
|
|
438
464
|
f'font-size:12px;color:{_FAINT};mso-line-height-rule:exactly;line-height:16px;">'
|
|
439
|
-
f"Sent by detectkit{cc_html}</td></tr>"
|
|
465
|
+
f"Sent by detectkit{project_html}{cc_html}</td></tr>"
|
|
440
466
|
)
|
|
441
467
|
|
|
442
468
|
def format_mentions(self, mentions: list[str]) -> str:
|
|
@@ -148,7 +148,12 @@ class TelegramChannel(BaseAlertChannel):
|
|
|
148
148
|
return html.escape(str(value))
|
|
149
149
|
|
|
150
150
|
metric = esc(ctx["metric_name"])
|
|
151
|
-
|
|
151
|
+
# Lead the headline with the project name (when set) so multiple
|
|
152
|
+
# projects sharing one chat stay distinguishable — Telegram has no
|
|
153
|
+
# footer/avatar override, so this prefix is the only project cue.
|
|
154
|
+
proj = ctx["project_name"]
|
|
155
|
+
head_prefix = f"[{esc(proj)}] " if proj else ""
|
|
156
|
+
lines: list[str] = [f"{dot} <b>{head_prefix}{word} · {metric}</b>"]
|
|
152
157
|
|
|
153
158
|
if ctx["description"]:
|
|
154
159
|
lines.append(f"<i>{esc(self._cap(ctx['description'], _DESC_CAP))}</i>")
|
|
@@ -189,8 +189,13 @@ class WebhookChannel(BaseAlertChannel):
|
|
|
189
189
|
attachment["title_link"] = alert_data.dashboard_url
|
|
190
190
|
|
|
191
191
|
# Brand the attachment footer (reliable on Slack even when top-level
|
|
192
|
-
# username/icon are locked to the app install).
|
|
193
|
-
|
|
192
|
+
# username/icon are locked to the app install). Pair the brand name with
|
|
193
|
+
# the project name when set ("detectkit · my_project") so two projects
|
|
194
|
+
# posting to the same channel stay distinguishable even past the title.
|
|
195
|
+
footer = self.username or BRAND_USERNAME
|
|
196
|
+
if alert_data.project_name:
|
|
197
|
+
footer = f"{footer} · {alert_data.project_name}"
|
|
198
|
+
attachment["footer"] = footer
|
|
194
199
|
if self.icon_url:
|
|
195
200
|
attachment["footer_icon"] = self.icon_url
|
|
196
201
|
# Slack-only sugar: a real timestamp under the footer. Mattermost
|
|
@@ -25,6 +25,7 @@ class _OrchestratorBase:
|
|
|
25
25
|
mentions: list[str] | None = None,
|
|
26
26
|
dashboard_url: str | None = None,
|
|
27
27
|
links: dict[str, str] | None = None,
|
|
28
|
+
project_name: str | None = None,
|
|
28
29
|
):
|
|
29
30
|
self.metric_name = metric_name
|
|
30
31
|
self.interval = interval
|
|
@@ -37,6 +38,11 @@ class _OrchestratorBase:
|
|
|
37
38
|
self.mentions = mentions or []
|
|
38
39
|
self.dashboard_url = dashboard_url
|
|
39
40
|
self.links = links or {}
|
|
41
|
+
# Optional project name (``detectkit_project.yml`` ``name``). Stamped
|
|
42
|
+
# onto every AlertData so channels can label which project an alert
|
|
43
|
+
# came from — keeps multiple projects sharing one channel distinct
|
|
44
|
+
# while the bot keeps the default brand name + avatar.
|
|
45
|
+
self.project_name = project_name
|
|
40
46
|
|
|
41
47
|
@staticmethod
|
|
42
48
|
def _group_by_timestamp(
|
|
@@ -241,6 +241,7 @@ class _DecisionMixin(_OrchestratorBase):
|
|
|
241
241
|
mentions=self.mentions,
|
|
242
242
|
dashboard_url=self.dashboard_url,
|
|
243
243
|
links=self.links,
|
|
244
|
+
project_name=self.project_name,
|
|
244
245
|
# Alert rule the message foregrounds: configured thresholds plus
|
|
245
246
|
# the observed quorum size that satisfied them.
|
|
246
247
|
min_detectors=self.conditions.min_detectors,
|
|
@@ -300,6 +301,7 @@ class _DecisionMixin(_OrchestratorBase):
|
|
|
300
301
|
mentions=self.mentions,
|
|
301
302
|
dashboard_url=self.dashboard_url,
|
|
302
303
|
links=self.links,
|
|
304
|
+
project_name=self.project_name,
|
|
303
305
|
)
|
|
304
306
|
|
|
305
307
|
def get_last_complete_point(self, now: datetime | None = None) -> datetime:
|
|
@@ -178,6 +178,7 @@ class _RecoveryMixin(_OrchestratorBase):
|
|
|
178
178
|
mentions=self.mentions,
|
|
179
179
|
dashboard_url=self.dashboard_url,
|
|
180
180
|
links=self.links,
|
|
181
|
+
project_name=self.project_name,
|
|
181
182
|
# Echo the rule that had fired so the recovery message names the
|
|
182
183
|
# same alert condition that just cleared.
|
|
183
184
|
min_detectors=self.conditions.min_detectors,
|
|
@@ -158,7 +158,9 @@ With no custom `template`, each channel renders a structured, branded message
|
|
|
158
158
|
shared value computation lives in one place (`BaseAlertChannel.build_context`),
|
|
159
159
|
so templates and native rendering stay consistent. Every alert title/headline
|
|
160
160
|
leads with a colored **status circle** — 🔴 anomaly, 🟢 recovery, 🟡 no-data,
|
|
161
|
-
🔵 pipeline error — so the status reads from color alone.
|
|
161
|
+
🔵 pipeline error — so the status reads from color alone. It also leads with the
|
|
162
|
+
**project name** as a `[name] ` prefix (from `detectkit_project.yml`) — see
|
|
163
|
+
[Project label](#project-label-multi-project-channels) below.
|
|
162
164
|
|
|
163
165
|
- **Slack / Mattermost / generic webhook** — one message *attachment* with a
|
|
164
166
|
status-colored accent bar, a clickable title (the metric; links to
|
|
@@ -181,6 +183,25 @@ leads with a colored **status circle** — 🔴 anomaly, 🟢 recovery, 🟡 no-
|
|
|
181
183
|
table, a monospace params box, an optional "Open dashboard" button, and a
|
|
182
184
|
footer. The plain-text body remains the multipart fallback.
|
|
183
185
|
|
|
186
|
+
## Project label (multi-project channels)
|
|
187
|
+
|
|
188
|
+
The bot keeps the **detectkit brand** name + avatar by default (so users rarely
|
|
189
|
+
override them). To still tell apart two projects posting to the **same** channel,
|
|
190
|
+
detectkit stamps the project name (`detectkit_project.yml` → `name`) onto every
|
|
191
|
+
alert and shows it by default — no config needed:
|
|
192
|
+
|
|
193
|
+
- **Title / headline / subject** lead with a `[name] ` prefix on every kind
|
|
194
|
+
(anomaly, recovery, no-data, error): `🔴 [payments] Alert: api_error_rate`.
|
|
195
|
+
- **Webhook (Slack/Mattermost)** also pairs it in the footer: `detectkit · payments`.
|
|
196
|
+
- **Telegram** carries it in the bold headline (no footer/avatar to override).
|
|
197
|
+
- **Email** prefixes the subject, shows a small project eyebrow above the metric,
|
|
198
|
+
and pairs it in the footer (`Sent by detectkit · payments`).
|
|
199
|
+
|
|
200
|
+
It is exposed to custom templates as `{project_name}` and `{project_name_prefix}`
|
|
201
|
+
(`"[name] "` when set, else `""`). Direct library/API callers that don't set it
|
|
202
|
+
render unchanged. The `name` is informational only (it does not key any `_dtk_*`
|
|
203
|
+
table), so renaming it is safe — spaces are allowed for a prettier label.
|
|
204
|
+
|
|
184
205
|
## Multiple alert configs per metric
|
|
185
206
|
|
|
186
207
|
`alerting:` may be a **list** of independent blocks, each with its own channels,
|
|
@@ -210,6 +231,7 @@ referenced by path). Key variables:
|
|
|
210
231
|
| Variable | Meaning |
|
|
211
232
|
|---|---|
|
|
212
233
|
| `{metric_name}`, `{description}` / `{description_line}` | identity |
|
|
234
|
+
| `{project_name}` / `{project_name_prefix}` | project label (`"[name] "` prefix, or `""`) |
|
|
213
235
|
| `{timestamp}`, `{timezone}` | when (display tz via `alerting.timezone`, default UTC) |
|
|
214
236
|
| `{value}` / `{value_display}` | metric value (`value_display` is NaN-safe) |
|
|
215
237
|
| `{confidence_lower}` / `{confidence_upper}` / `{confidence_interval}` | bounds |
|
|
@@ -9,7 +9,9 @@ errors (not empty strings).
|
|
|
9
9
|
## `detectkit_project.yml`
|
|
10
10
|
|
|
11
11
|
```yaml
|
|
12
|
-
name: my_monitoring # required — project identifier
|
|
12
|
+
name: my_monitoring # required — project identifier; also labels every
|
|
13
|
+
# alert ("[my_monitoring] Alert: …") so multiple
|
|
14
|
+
# projects on one channel stay distinct (alerting.md)
|
|
13
15
|
version: "1.0" # optional (default "1.0")
|
|
14
16
|
default_profile: prod # profile name from profiles.yml
|
|
15
17
|
|
|
@@ -57,8 +59,10 @@ error_alerting:
|
|
|
57
59
|
via cron cadence). Channel send failures are swallowed so a flaky webhook
|
|
58
60
|
can't crash the run.
|
|
59
61
|
- Extra template variables: `{error_type}`, `{error_message}`, `{status}`
|
|
60
|
-
(always `"ERROR"`)
|
|
61
|
-
`"[<name>] "` when `name` set
|
|
62
|
+
(always `"ERROR"`). `{project_name}` / `{project_name_prefix}` (=
|
|
63
|
+
`"[<name>] "` when `name` set) are available here **and in every other alert
|
|
64
|
+
template** — and by default lead the title/headline on all channels, keeping
|
|
65
|
+
multi-project channels distinguishable (see `alerting.md` → Project label).
|
|
62
66
|
|
|
63
67
|
## `profiles.yml`
|
|
64
68
|
|
|
@@ -64,6 +64,7 @@ class _AlertStepMixin(_TaskManagerBase):
|
|
|
64
64
|
mentions=alerting_config.mentions,
|
|
65
65
|
dashboard_url=alerting_config.dashboard_url,
|
|
66
66
|
links=alerting_config.links,
|
|
67
|
+
project_name=getattr(self.project_config, "name", None),
|
|
67
68
|
)
|
|
68
69
|
|
|
69
70
|
last_point = orchestrator.get_last_complete_point()
|
|
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.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/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
|
|
File without changes
|