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.
Files changed (105) hide show
  1. {detectkit-0.14.0/detectkit.egg-info → detectkit-0.15.0}/PKG-INFO +1 -1
  2. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/__init__.py +1 -1
  3. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/base.py +15 -11
  4. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/email.py +31 -5
  5. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/telegram.py +6 -1
  6. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/webhook.py +7 -2
  7. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_base.py +6 -0
  8. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_decision.py +2 -0
  9. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_recovery.py +1 -0
  10. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/alerting.md +23 -1
  11. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/project.md +7 -3
  12. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_alert_step.py +1 -0
  13. {detectkit-0.14.0 → detectkit-0.15.0/detectkit.egg-info}/PKG-INFO +1 -1
  14. {detectkit-0.14.0 → detectkit-0.15.0}/LICENSE +0 -0
  15. {detectkit-0.14.0 → detectkit-0.15.0}/MANIFEST.in +0 -0
  16. {detectkit-0.14.0 → detectkit-0.15.0}/README.md +0 -0
  17. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/__init__.py +0 -0
  18. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/__init__.py +0 -0
  19. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/branding.py +0 -0
  20. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/factory.py +0 -0
  21. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/mattermost.py +0 -0
  22. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/channels/slack.py +0 -0
  23. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  24. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  25. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  26. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  27. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  28. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/__init__.py +0 -0
  29. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/_output.py +0 -0
  30. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  31. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  32. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  33. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  34. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  35. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  36. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  37. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  38. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/__init__.py +0 -0
  39. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/clean.py +0 -0
  40. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/init.py +0 -0
  41. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/init_claude.py +0 -0
  42. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/run.py +0 -0
  43. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/test_alert.py +0 -0
  44. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/commands/unlock.py +0 -0
  45. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/cli/main.py +0 -0
  46. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/config/__init__.py +0 -0
  47. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/config/metric_config.py +0 -0
  48. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/config/profile.py +0 -0
  49. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/config/project_config.py +0 -0
  50. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/config/validator.py +0 -0
  51. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/core/__init__.py +0 -0
  52. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/core/interval.py +0 -0
  53. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/core/models.py +0 -0
  54. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/__init__.py +0 -0
  55. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/_sql_manager.py +0 -0
  56. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/clickhouse_manager.py +0 -0
  57. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/__init__.py +0 -0
  58. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  59. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_base.py +0 -0
  60. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  61. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_detections.py +0 -0
  62. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  63. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  64. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_schema.py +0 -0
  65. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  66. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/internal_tables/manager.py +0 -0
  67. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/manager.py +0 -0
  68. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/mysql_manager.py +0 -0
  69. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/postgres_manager.py +0 -0
  70. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/database/tables.py +0 -0
  71. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/__init__.py +0 -0
  72. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/base.py +0 -0
  73. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/factory.py +0 -0
  74. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/seasonality.py +0 -0
  75. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/statistical/__init__.py +0 -0
  76. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  77. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/statistical/iqr.py +0 -0
  78. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/statistical/mad.py +0 -0
  79. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  80. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/detectors/statistical/zscore.py +0 -0
  81. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/loaders/__init__.py +0 -0
  82. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/loaders/metric_loader.py +0 -0
  83. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/loaders/query_template.py +0 -0
  84. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/__init__.py +0 -0
  85. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/error_dispatch.py +0 -0
  86. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  87. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  88. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  89. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  90. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  91. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  92. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/utils/__init__.py +0 -0
  93. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/utils/datetime_utils.py +0 -0
  94. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/utils/env_interpolation.py +0 -0
  95. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/utils/json_utils.py +0 -0
  96. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit/utils/stats.py +0 -0
  97. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit.egg-info/SOURCES.txt +0 -0
  98. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit.egg-info/dependency_links.txt +0 -0
  99. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit.egg-info/entry_points.txt +0 -0
  100. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit.egg-info/requires.txt +0 -0
  101. {detectkit-0.14.0 → detectkit-0.15.0}/detectkit.egg-info/top_level.txt +0 -0
  102. {detectkit-0.14.0 → detectkit-0.15.0}/pyproject.toml +0 -0
  103. {detectkit-0.14.0 → detectkit-0.15.0}/requirements.txt +0 -0
  104. {detectkit-0.14.0 → detectkit-0.15.0}/setup.cfg +0 -0
  105. {detectkit-0.14.0 → detectkit-0.15.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.14.0
3
+ Version: 0.15.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.14.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
- as ``{project_name}`` in templates and as a ``[name] `` prefix
37
- in the default error title. Lets multiple projects share the
38
- same alert channel without ambiguity.
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 it reaches the Subject header
148
- # so a metric name can never inject extra email headers.
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
- msg["Subject"] = self.subject_template.format(metric_name=subject_metric)
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};">&nbsp;</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" &middot; {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" &middot; {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
- lines: list[str] = [f"{dot} <b>{word} · {metric}</b>"]
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
- attachment["footer"] = self.username or BRAND_USERNAME
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 (logs, error-alert titles)
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"`), `{project_name}`, `{project_name_prefix}` (=
61
- `"[<name>] "` when `name` set keeps multi-project channels distinguishable).
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.14.0
3
+ Version: 0.15.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes